vedo.volume

Work with volumetric datasets (voxel data).

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

Class to describe dataset that are defined on "voxels", the 3D equivalent of 2D pixels.

Volume(input_obj=None, dims=None, origin=None, spacing=None)
 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                ![](https://vedo.embl.es/images/volumetric/numpy2volume1.png)
 74
 75            - [read_volume2.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/read_volume2.py)
 76
 77                ![](https://vedo.embl.es/images/volumetric/read_volume2.png)
 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                self.alpha([0.0, 0.0, 0.2, 0.4, 0.8, 1.0])
195                self.alpha_gradient(None)
196                self.properties.SetScalarOpacityUnitDistance(1.0)
197
198        self.pipeline = utils.OperationNode(
199            "Volume", comment=f"dims={tuple(self.dimensions())}", c="#4cc9f0"
200        )
201        #######################################################################

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 the Volume(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.

mapper
203    @property
204    def mapper(self):
205        """Return the underlying `vtkMapper` object."""
206        return self.actor.GetMapper()

Return the underlying vtkMapper object.

def c(self, *args, **kwargs) -> Self:
237    def c(self, *args, **kwargs) -> Self:
238        """Deprecated. Use `Volume.cmap()` instead."""
239        vedo.logger.warning("Volume.c() is deprecated, use Volume.cmap() instead")
240        return self.cmap(*args, **kwargs)

Deprecated. Use Volume.cmap() instead.

def copy(self, deep=True) -> Volume:
379    def copy(self, deep=True) -> "Volume":
380        """Return a copy of the Volume. Alias of `clone()`."""
381        return self.clone(deep=deep)

Return a copy of the Volume. Alias of clone().

def clone(self, deep=True) -> Volume:
383    def clone(self, deep=True) -> "Volume":
384        """Return a clone copy of the Volume. Alias of `copy()`."""
385        if deep:
386            newimg = vtki.vtkImageData()
387            newimg.CopyStructure(self.dataset)
388            newimg.CopyAttributes(self.dataset)
389            newvol = Volume(newimg)
390        else:
391            newvol = Volume(self.dataset)
392
393        prop = vtki.vtkVolumeProperty()
394        prop.DeepCopy(self.properties)
395        newvol.actor.SetProperty(prop)
396        newvol.properties = prop
397
398        newvol.pipeline = utils.OperationNode("clone", parents=[self], c="#bbd0ff", shape="diamond")
399        return newvol

Return a clone copy of the Volume. Alias of copy().

def astype(self, dtype: Union[str, int]) -> Self:
401    def astype(self, dtype: Union[str, int]) -> Self:
402        """
403        Reset the type of the scalars array.
404
405        Arguments:
406            dtype : (str)
407                the type of the scalars array in
408                ["int8", "uint8", "int16", "uint16", "int32", "uint32", "float32", "float64"]
409        """
410        if dtype in ["int8", "uint8", "int16", "uint16", "int32", "uint32", "float32", "float64"]:
411            caster = vtki.new("ImageCast")
412            caster.SetInputData(self.dataset)
413            caster.SetOutputScalarType(int(vtki.array_types[dtype]))
414            caster.ClampOverflowOn()
415            caster.Update()
416            self._update(caster.GetOutput())
417            self.pipeline = utils.OperationNode(f"astype({dtype})", parents=[self], c="#4cc9f0")
418        else:
419            vedo.logger.error(f"astype(): unknown type {dtype}")
420            raise ValueError()
421        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"]
def component_weight(self, i: int, weight: float) -> Self:
423    def component_weight(self, i: int, weight: float) -> Self:
424        """Set the scalar component weight in range [0,1]."""
425        self.properties.SetComponentWeight(i, weight)
426        return self

Set the scalar component weight in range [0,1].

def xslice(self, i: int) -> vedo.mesh.Mesh:
428    def xslice(self, i: int) -> Mesh:
429        """Extract the slice at index `i` of volume along x-axis."""
430        vslice = vtki.new("ImageDataGeometryFilter")
431        vslice.SetInputData(self.dataset)
432        nx, ny, nz = self.dataset.GetDimensions()
433        if i > nx - 1:
434            i = nx - 1
435        vslice.SetExtent(i, i, 0, ny, 0, nz)
436        vslice.Update()
437        m = Mesh(vslice.GetOutput())
438        m.pipeline = utils.OperationNode(f"xslice {i}", parents=[self], c="#4cc9f0:#e9c46a")
439        return m

Extract the slice at index i of volume along x-axis.

def yslice(self, j: int) -> vedo.mesh.Mesh:
441    def yslice(self, j: int) -> Mesh:
442        """Extract the slice at index `j` of volume along y-axis."""
443        vslice = vtki.new("ImageDataGeometryFilter")
444        vslice.SetInputData(self.dataset)
445        nx, ny, nz = self.dataset.GetDimensions()
446        if j > ny - 1:
447            j = ny - 1
448        vslice.SetExtent(0, nx, j, j, 0, nz)
449        vslice.Update()
450        m = Mesh(vslice.GetOutput())
451        m.pipeline = utils.OperationNode(f"yslice {j}", parents=[self], c="#4cc9f0:#e9c46a")
452        return m

Extract the slice at index j of volume along y-axis.

def zslice(self, k: int) -> vedo.mesh.Mesh:
454    def zslice(self, k: int) -> Mesh:
455        """Extract the slice at index `i` of volume along z-axis."""
456        vslice = vtki.new("ImageDataGeometryFilter")
457        vslice.SetInputData(self.dataset)
458        nx, ny, nz = self.dataset.GetDimensions()
459        if k > nz - 1:
460            k = nz - 1
461        vslice.SetExtent(0, nx, 0, ny, k, k)
462        vslice.Update()
463        m = Mesh(vslice.GetOutput())
464        m.pipeline = utils.OperationNode(f"zslice {k}", parents=[self], c="#4cc9f0:#e9c46a")
465        return m

Extract the slice at index i of volume along z-axis.

def slice_plane( self, origin: List[float], normal: List[float], autocrop=False, border=0.5, mode='linear') -> vedo.mesh.Mesh:
467    def slice_plane(self, origin: List[float], normal: List[float], autocrop=False, border=0.5, mode="linear") -> Mesh:
468        """
469        Extract the slice along a given plane position and normal.
470
471        Two metadata arrays are added to the output Mesh:
472            - "shape" : contains the shape of the slice
473            - "original_bounds" : contains the original bounds of the slice
474        One can access them with e.g. `myslice.metadata["shape"]`.
475
476        Arguments:
477            origin : (list)
478                position of the plane
479            normal : (list)
480                normal to the plane
481            autocrop : (bool)
482                crop the output to the minimal possible size
483            border : (float)
484                add a border to the output slice
485            mode : (str)
486                interpolation mode, one of the following: "linear", "nearest", "cubic"
487
488        Example:
489            - [slice_plane1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slice_plane1.py)
490
491                ![](https://vedo.embl.es/images/volumetric/slicePlane1.gif)
492            
493            - [slice_plane2.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slice_plane2.py)
494                
495                ![](https://vedo.embl.es/images/volumetric/slicePlane2.png)
496
497            - [slice_plane3.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slice_plane3.py)
498
499                ![](https://vedo.embl.es/images/volumetric/slicePlane3.jpg)
500        """
501        newaxis = utils.versor(normal)
502        pos = np.array(origin)
503        initaxis = (0, 0, 1)
504        crossvec = np.cross(initaxis, newaxis)
505        angle = np.arccos(np.dot(initaxis, newaxis))
506        T = vtki.vtkTransform()
507        T.PostMultiply()
508        T.RotateWXYZ(np.rad2deg(angle), crossvec.tolist())
509        T.Translate(pos.tolist())
510
511        reslice = vtki.new("ImageReslice")
512        reslice.SetResliceAxes(T.GetMatrix())
513        reslice.SetInputData(self.dataset)
514        reslice.SetOutputDimensionality(2)
515        reslice.SetTransformInputSampling(True)
516        reslice.SetGenerateStencilOutput(False)
517        if border:
518            reslice.SetBorder(True)
519            reslice.SetBorderThickness(border)
520        else:
521            reslice.SetBorder(False)
522        if mode == "linear":
523            reslice.SetInterpolationModeToLinear()
524        elif mode == "nearest":
525            reslice.SetInterpolationModeToNearestNeighbor()
526        elif mode == "cubic":
527            reslice.SetInterpolationModeToCubic()
528        else:
529            vedo.logger.error(f"in slice_plane(): unknown interpolation mode {mode}")
530            raise ValueError()
531        reslice.SetAutoCropOutput(not autocrop)
532        reslice.Update()
533        img = reslice.GetOutput()
534
535        vslice = vtki.new("ImageDataGeometryFilter")
536        vslice.SetInputData(img)
537        vslice.Update()
538
539        msh = Mesh(vslice.GetOutput()).apply_transform(T)
540        msh.properties.LightingOff()
541
542        d0, d1, _ = img.GetDimensions()
543        varr1 = utils.numpy2vtk([d1, d0], name="shape")
544        varr2 = utils.numpy2vtk(img.GetBounds(), name="original_bounds")
545        msh.dataset.GetFieldData().AddArray(varr1)
546        msh.dataset.GetFieldData().AddArray(varr2)
547        msh.pipeline = utils.OperationNode("slice_plane", parents=[self], c="#4cc9f0:#e9c46a")
548        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:
def slab(self, slice_range=(), axis='z', operation='mean') -> vedo.mesh.Mesh:
550    def slab(self, slice_range=(), axis='z', operation="mean") -> Mesh:
551        """
552        Extract a slab from a `Volume` by combining 
553        all of the slices of an image to create a single slice.
554
555        Returns a `Mesh` containing metadata which
556        can be accessed with e.g. `mesh.metadata["slab_range"]`.
557
558        Metadata:
559            slab_range : (list)
560                contains the range of slices extracted
561            slab_axis : (str)
562                contains the axis along which the slab was extracted
563            slab_operation : (str)
564                contains the operation performed on the slab
565            slab_bounding_box : (list)
566                contains the bounding box of the slab
567
568        Arguments:
569            slice_range : (list)
570                range of slices to extract
571            axis : (str)
572                axis along which to extract the slab
573            operation : (str)
574                operation to perform on the slab,
575                allowed values are: "sum", "min", "max", "mean".
576        
577        Example:
578            - [slab.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/slab_vol.py)
579
580            ![](https://vedo.embl.es/images/volumetric/slab_vol.jpg)
581        """
582        if len(slice_range) != 2:
583            vedo.logger.error("in slab(): slice_range is empty or invalid")
584            raise ValueError()
585        
586        islab = vtki.new("ImageSlab")
587        islab.SetInputData(self.dataset)
588
589        if operation in ["+", "add", "sum"]:
590            islab.SetOperationToSum()
591        elif "min" in operation:
592            islab.SetOperationToMin()
593        elif "max" in operation:
594            islab.SetOperationToMax()
595        elif "mean" in operation:
596            islab.SetOperationToMean()
597        else:
598            vedo.logger.error(f"in slab(): unknown operation {operation}")
599            raise ValueError()
600
601        dims = self.dimensions()
602        if axis == 'x':
603            islab.SetOrientationToX()
604            if slice_range[0]  > dims[0]-1:
605                slice_range[0] = dims[0]-1
606            if slice_range[1]  > dims[0]-1:
607                slice_range[1] = dims[0]-1
608        elif axis == 'y':
609            islab.SetOrientationToY()
610            if slice_range[0]  > dims[1]-1:
611                slice_range[0] = dims[1]-1
612            if slice_range[1]  > dims[1]-1:
613                slice_range[1] = dims[1]-1
614        elif axis == 'z':
615            islab.SetOrientationToZ()
616            if slice_range[0]  > dims[2]-1:
617                slice_range[0] = dims[2]-1
618            if slice_range[1]  > dims[2]-1:
619                slice_range[1] = dims[2]-1
620        else:
621            vedo.logger.error(f"Error in slab(): unknown axis {axis}")
622            raise RuntimeError()
623        
624        islab.SetSliceRange(slice_range)
625        islab.Update()
626
627        msh = Mesh(islab.GetOutput()).lighting('off')
628        msh.mapper.SetLookupTable(utils.ctf2lut(self, msh))
629        msh.mapper.SetScalarRange(self.scalar_range())
630
631        msh.metadata["slab_range"] = slice_range
632        msh.metadata["slab_axis"]  = axis
633        msh.metadata["slab_operation"] = operation
634
635        # compute bounds of slab
636        origin = list(self.origin())
637        spacing = list(self.spacing())
638        if axis == 'x':
639            msh.metadata["slab_bounding_box"] = [
640                origin[0] + slice_range[0]*spacing[0],
641                origin[0] + slice_range[1]*spacing[0],
642                origin[1],
643                origin[1] + dims[1]*spacing[1],
644                origin[2],
645                origin[2] + dims[2]*spacing[2],
646            ]
647        elif axis == 'y':
648            msh.metadata["slab_bounding_box"] = [
649                origin[0],
650                origin[0] + dims[0]*spacing[0],
651                origin[1] + slice_range[0]*spacing[1],
652                origin[1] + slice_range[1]*spacing[1],
653                origin[2],
654                origin[2] + dims[2]*spacing[2],
655            ]
656        elif axis == 'z':
657            msh.metadata["slab_bounding_box"] = [
658                origin[0],
659                origin[0] + dims[0]*spacing[0],
660                origin[1],
661                origin[1] + dims[1]*spacing[1],
662                origin[2] + slice_range[0]*spacing[2],
663                origin[2] + slice_range[1]*spacing[2],
664            ]
665
666        msh.pipeline = utils.OperationNode(
667            f"slab{slice_range}", 
668            comment=f"axis={axis}, operation={operation}",
669            parents=[self],
670            c="#4cc9f0:#e9c46a",
671        )
672        msh.name = "SlabMesh"
673        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:

def warp( self, source: Union[vedo.pointcloud.Points, List], target: Union[vedo.pointcloud.Points, List], sigma=1, mode='3d', fit=True) -> Self:
676    def warp(
677            self,
678            source: Union["vedo.Points", List],
679            target: Union["vedo.Points", List],
680            sigma=1, mode="3d", fit=True,
681        ) -> Self:
682        """
683        Warp volume scalars within a Volume by specifying
684        source and target sets of points.
685
686        Arguments:
687            source : (Points, list)
688                the list of source points
689            target : (Points, list)
690                the list of target points
691            fit : (bool)
692                fit/adapt the old bounding box to the warped geometry
693        """
694        if isinstance(source, vedo.Points):
695            source = source.vertices
696        if isinstance(target, vedo.Points):
697            target = target.vertices
698
699        NLT = transformations.NonLinearTransform()
700        NLT.source_points = source
701        NLT.target_points = target
702        NLT.sigma = sigma
703        NLT.mode = mode
704
705        self.apply_transform(NLT, fit=fit)
706        self.pipeline = utils.OperationNode("warp", parents=[self], c="#4cc9f0")
707        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
def apply_transform( self, T: Union[vedo.transformations.LinearTransform, vedo.transformations.NonLinearTransform], fit=True, interpolation='cubic') -> Self:
709    def apply_transform(
710            self, 
711            T: Union[transformations.LinearTransform, transformations.NonLinearTransform],
712            fit=True, interpolation="cubic",
713        ) -> Self:
714        """
715        Apply a transform to the scalars in the volume.
716
717        Arguments:
718            T : (LinearTransform, NonLinearTransform)
719                The transformation to be applied
720            fit : (bool)
721                fit/adapt the old bounding box to the modified geometry
722            interpolation : (str)
723                one of the following: "nearest", "linear", "cubic"
724        """
725        if utils.is_sequence(T):
726            T = transformations.LinearTransform(T)
727
728        TI = T.compute_inverse()
729
730        reslice = vtki.new("ImageReslice")
731        reslice.SetInputData(self.dataset)
732        reslice.SetResliceTransform(TI.T)
733        reslice.SetOutputDimensionality(3)
734        if "lin" in interpolation.lower():
735            reslice.SetInterpolationModeToLinear()
736        elif "near" in interpolation.lower():
737            reslice.SetInterpolationModeToNearestNeighbor()
738        elif "cubic" in interpolation.lower():
739            reslice.SetInterpolationModeToCubic()
740        else:
741            vedo.logger.error(
742                f"in apply_transform: unknown interpolation mode {interpolation}")
743            raise ValueError()
744        reslice.SetAutoCropOutput(fit)
745        reslice.Update()
746        self._update(reslice.GetOutput())
747        self.transform = T
748        self.pipeline = utils.OperationNode(
749            "apply_transform", parents=[self], c="#4cc9f0")
750        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"
def imagedata(self) -> vtkmodules.vtkCommonDataModel.vtkImageData:
752    def imagedata(self) -> vtki.vtkImageData:
753        """
754        DEPRECATED:
755        Use `Volume.dataset` instead.
756
757        Return the underlying `vtkImagaData` object.
758        """
759        print("Volume.imagedata() is deprecated, use Volume.dataset instead")
760        return self.dataset

DEPRECATED: Use Volume.dataset instead.

Return the underlying vtkImagaData object.

def modified(self) -> Self:
762    def modified(self) -> Self:
763        """
764        Mark the object as modified.
765
766        Example:
767
768        - [numpy2volume0.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/numpy2volume0.py)
769        """
770        scals = self.dataset.GetPointData().GetScalars()
771        if scals:
772            scals.Modified()
773        return self

Mark the object as modified.

Example:

def tonumpy(self) -> numpy.ndarray:
775    def tonumpy(self) -> np.ndarray:
776        """
777        Get read-write access to voxels of a Volume object as a numpy array.
778
779        When you set values in the output image, you don't want numpy to reallocate the array
780        but instead set values in the existing array, so use the [:] operator.
781
782        Example:
783            `arr[:] = arr*2 + 15`
784
785        If the array is modified add a call to:
786        `volume.modified()`
787        when all your modifications are completed.
788        """
789        narray_shape = tuple(reversed(self.dataset.GetDimensions()))
790
791        scals = self.dataset.GetPointData().GetScalars()
792        comps = scals.GetNumberOfComponents()
793        if comps == 1:
794            narray = utils.vtk2numpy(scals).reshape(narray_shape)
795            narray = np.transpose(narray, axes=[2, 1, 0])
796        else:
797            narray = utils.vtk2numpy(scals).reshape(*narray_shape, comps)
798            narray = np.transpose(narray, axes=[2, 1, 0, 3])
799
800        # narray = utils.vtk2numpy(self.dataset.GetPointData().GetScalars()).reshape(narray_shape)
801        # narray = np.transpose(narray, axes=[2, 1, 0])
802        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.

shape: numpy.ndarray
804    @property
805    def shape(self) -> np.ndarray:
806        """Return the nr. of voxels in the 3 dimensions."""
807        return np.array(self.dataset.GetDimensions())

Return the nr. of voxels in the 3 dimensions.

def dimensions(self) -> numpy.ndarray:
809    def dimensions(self) -> np.ndarray:
810        """Return the nr. of voxels in the 3 dimensions."""
811        return np.array(self.dataset.GetDimensions())

Return the nr. of voxels in the 3 dimensions.

def scalar_range(self) -> numpy.ndarray:
813    def scalar_range(self) -> np.ndarray:
814        """Return the range of the scalar values."""
815        return np.array(self.dataset.GetScalarRange())

Return the range of the scalar values.

def spacing(self, s=None) -> Union[Self, Iterable[float]]:
817    def spacing(self, s=None) -> Union[Self, Iterable[float]]:
818        """Set/get the voxels size in the 3 dimensions."""
819        if s is not None:
820            self.dataset.SetSpacing(s)
821            return self
822        return np.array(self.dataset.GetSpacing())

Set/get the voxels size in the 3 dimensions.

def origin(self, s=None) -> Union[Self, Iterable[float]]:
824    def origin(self, s=None) -> Union[Self, Iterable[float]]:
825        """
826        Set/get the origin of the volumetric dataset.
827
828        The origin is the position in world coordinates of the point index (0,0,0).
829        This point does not have to be part of the dataset, in other words,
830        the dataset extent does not have to start at (0,0,0) and the origin 
831        can be outside of the dataset bounding box. 
832        The origin plus spacing determine the position in space of the points.
833        """
834        if s is not None:
835            self.dataset.SetOrigin(s)
836            return self
837        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.

def pos(self, p=None) -> Union[Self, Iterable[float]]:
839    def pos(self, p=None) -> Union[Self, Iterable[float]]:
840        """Set/get the position of the volumetric dataset."""
841        if p is not None:
842            self.origin(p)
843            return self
844        return self.origin()

Set/get the position of the volumetric dataset.

def center(self) -> numpy.ndarray:
846    def center(self) -> np.ndarray:
847        """Get the center of the volumetric dataset."""
848        # note that this does not have the set method like origin and spacing
849        return np.array(self.dataset.GetCenter())

Get the center of the volumetric dataset.

def shift(self, s: list) -> Self:
851    def shift(self, s: list) -> Self:
852        """Shift the volumetric dataset by a vector."""
853        self.origin(self.origin() + np.array(s))
854        return self

Shift the volumetric dataset by a vector.

def rotate_x(self, angle: float, rad=False, around=None) -> Self:
856    def rotate_x(self, angle: float, rad=False, around=None) -> Self:
857        """
858        Rotate around x-axis. If angle is in radians set `rad=True`.
859
860        Use `around` to define a pivoting point.
861        """
862        if angle == 0:
863            return self
864        LT = transformations.LinearTransform().rotate_x(angle, rad, around)
865        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.

def rotate_y(self, angle: float, rad=False, around=None) -> Self:
867    def rotate_y(self, angle: float, rad=False, around=None) -> Self:
868        """
869        Rotate around y-axis. If angle is in radians set `rad=True`.
870
871        Use `around` to define a pivoting point.
872        """
873        if angle == 0:
874            return self
875        LT = transformations.LinearTransform().rotate_y(angle, rad, around)
876        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.

def rotate_z(self, angle: float, rad=False, around=None) -> Self:
878    def rotate_z(self, angle: float, rad=False, around=None) -> Self:
879        """
880        Rotate around z-axis. If angle is in radians set `rad=True`.
881
882        Use `around` to define a pivoting point.
883        """
884        if angle == 0:
885            return self
886        LT = transformations.LinearTransform().rotate_z(angle, rad, around)
887        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.

def get_cell_from_ijk(self, ijk: list) -> int:
889    def get_cell_from_ijk(self, ijk: list) -> int:
890        """
891        Get the voxel id number at the given ijk coordinates.
892
893        Arguments:
894            ijk : (list)
895                the ijk coordinates of the voxel
896        """
897        return self.dataset.ComputeCellId(ijk)

Get the voxel id number at the given ijk coordinates.

Arguments:
  • ijk : (list) the ijk coordinates of the voxel
def get_point_from_ijk(self, ijk: list) -> int:
899    def get_point_from_ijk(self, ijk: list) -> int:
900        """
901        Get the point id number at the given ijk coordinates.
902
903        Arguments:
904            ijk : (list)
905                the ijk coordinates of the voxel
906        """
907        return self.dataset.ComputePointId(ijk)

Get the point id number at the given ijk coordinates.

Arguments:
  • ijk : (list) the ijk coordinates of the voxel
def permute_axes(self, x: int, y: int, z: int) -> Self:
909    def permute_axes(self, x: int, y: int, z: int) -> Self:
910        """
911        Reorder the axes of the Volume by specifying
912        the input axes which are supposed to become the new X, Y, and Z.
913        """
914        imp = vtki.new("ImagePermute")
915        imp.SetFilteredAxes(x, y, z)
916        imp.SetInputData(self.dataset)
917        imp.Update()
918        self._update(imp.GetOutput())
919        self.pipeline = utils.OperationNode(
920            f"permute_axes({(x,y,z)})", parents=[self], c="#4cc9f0"
921        )
922        return self

Reorder the axes of the Volume by specifying the input axes which are supposed to become the new X, Y, and Z.

def resample(self, new_spacing: List[float], interpolation=1) -> Self:
924    def resample(self, new_spacing: List[float], interpolation=1) -> Self:
925        """
926        Resamples a `Volume` to be larger or smaller.
927
928        This method modifies the spacing of the input.
929        Linear interpolation is used to resample the data.
930
931        Arguments:
932            new_spacing : (list)
933                a list of 3 new spacings for the 3 axes
934            interpolation : (int)
935                0=nearest_neighbor, 1=linear, 2=cubic
936        """
937        rsp = vtki.new("ImageResample")
938        oldsp = self.spacing()
939        for i in range(3):
940            if oldsp[i] != new_spacing[i]:
941                rsp.SetAxisOutputSpacing(i, new_spacing[i])
942        rsp.InterpolateOn()
943        rsp.SetInterpolationMode(interpolation)
944        rsp.OptimizationOn()
945        rsp.Update()
946        self._update(rsp.GetOutput())
947        self.pipeline = utils.OperationNode(
948            "resample", comment=f"spacing: {tuple(new_spacing)}", parents=[self], c="#4cc9f0"
949        )
950        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
def threshold(self, above=None, below=None, replace=None, replace_value=None) -> Self:
953    def threshold(self, above=None, below=None, replace=None, replace_value=None) -> Self:
954        """
955        Binary or continuous volume thresholding.
956        Find the voxels that contain a value above/below the input values
957        and replace them with a new value (default is 0).
958        """
959        th = vtki.new("ImageThreshold")
960        th.SetInputData(self.dataset)
961
962        # sanity checks
963        if above is not None and below is not None:
964            if above == below:
965                return self
966            if above > below:
967                vedo.logger.warning("in volume.threshold(), above > below, skip.")
968                return self
969
970        ## cases
971        if below is not None and above is not None:
972            th.ThresholdBetween(above, below)
973
974        elif above is not None:
975            th.ThresholdByUpper(above)
976
977        elif below is not None:
978            th.ThresholdByLower(below)
979
980        ##
981        if replace is not None:
982            th.SetReplaceIn(True)
983            th.SetInValue(replace)
984        else:
985            th.SetReplaceIn(False)
986
987        if replace_value is not None:
988            th.SetReplaceOut(True)
989            th.SetOutValue(replace_value)
990        else:
991            th.SetReplaceOut(False)
992
993        th.Update()
994        self._update(th.GetOutput())
995        self.pipeline = utils.OperationNode("threshold", parents=[self], c="#4cc9f0")
996        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).

def crop( self, left=None, right=None, back=None, front=None, bottom=None, top=None, VOI=()) -> Self:
 998    def crop(self, left=None, right=None, back=None, front=None, bottom=None, top=None, VOI=()) -> Self:
 999        """
1000        Crop a `Volume` object.
1001
1002        Arguments:
1003            left : (float)
1004                fraction to crop from the left plane (negative x)
1005            right : (float)
1006                fraction to crop from the right plane (positive x)
1007            back : (float)
1008                fraction to crop from the back plane (negative y)
1009            front : (float)
1010                fraction to crop from the front plane (positive y)
1011            bottom : (float)
1012                fraction to crop from the bottom plane (negative z)
1013            top : (float)
1014                fraction to crop from the top plane (positive z)
1015            VOI : (list)
1016                extract Volume Of Interest expressed in voxel numbers
1017
1018        Example:
1019            `vol.crop(VOI=(xmin, xmax, ymin, ymax, zmin, zmax)) # all integers nrs`
1020        """
1021        extractVOI = vtki.new("ExtractVOI")
1022        extractVOI.SetInputData(self.dataset)
1023
1024        if VOI:
1025            extractVOI.SetVOI(VOI)
1026        else:
1027            d = self.dataset.GetDimensions()
1028            bx0, bx1, by0, by1, bz0, bz1 = 0, d[0]-1, 0, d[1]-1, 0, d[2]-1
1029            if left is not None:   bx0 = int((d[0]-1)*left)
1030            if right is not None:  bx1 = int((d[0]-1)*(1-right))
1031            if back is not None:   by0 = int((d[1]-1)*back)
1032            if front is not None:  by1 = int((d[1]-1)*(1-front))
1033            if bottom is not None: bz0 = int((d[2]-1)*bottom)
1034            if top is not None:    bz1 = int((d[2]-1)*(1-top))
1035            extractVOI.SetVOI(bx0, bx1, by0, by1, bz0, bz1)
1036        extractVOI.Update()
1037        self._update(extractVOI.GetOutput())
1038
1039        self.pipeline = utils.OperationNode(
1040            "crop", parents=[self], c="#4cc9f0", comment=f"dims={tuple(self.dimensions())}"
1041        )
1042        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

def append(self, *volumes, axis='z', preserve_extents=False) -> Self:
1044    def append(self, *volumes, axis="z", preserve_extents=False) -> Self:
1045        """
1046        Take the components from multiple inputs and merges them into one output.
1047        Except for the append axis, all inputs must have the same extent.
1048        All inputs must have the same number of scalar components.
1049        The output has the same origin and spacing as the first input.
1050        The origin and spacing of all other inputs are ignored.
1051        All inputs must have the same scalar type.
1052
1053        Arguments:
1054            axis : (int, str)
1055                axis expanded to hold the multiple images
1056            preserve_extents : (bool)
1057                if True, the extent of the inputs is used to place
1058                the image in the output. The whole extent of the output is the union of the input
1059                whole extents. Any portion of the output not covered by the inputs is set to zero.
1060                The origin and spacing is taken from the first input.
1061
1062        Example:
1063            ```python
1064            from vedo import Volume, dataurl
1065            vol = Volume(dataurl+'embryo.tif')
1066            vol.append(vol, axis='x').show().close()
1067            ```
1068            ![](https://vedo.embl.es/images/feats/volume_append.png)
1069        """
1070        ima = vtki.new("ImageAppend")
1071        ima.SetInputData(self.dataset)
1072        # if not utils.is_sequence(volumes):
1073        #     volumes = [volumes]
1074        for volume in volumes:
1075            if isinstance(volume, vtki.vtkImageData):
1076                ima.AddInputData(volume)
1077            else:
1078                ima.AddInputData(volume.dataset)
1079        ima.SetPreserveExtents(preserve_extents)
1080        if axis == "x":
1081            axis = 0
1082        elif axis == "y":
1083            axis = 1
1084        elif axis == "z":
1085            axis = 2
1086        ima.SetAppendAxis(axis)
1087        ima.Update()
1088        self._update(ima.GetOutput())
1089
1090        self.pipeline = utils.OperationNode(
1091            "append",
1092            parents=[self, *volumes],
1093            c="#4cc9f0",
1094            comment=f"dims={tuple(self.dimensions())}",
1095        )
1096        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()

def pad(self, voxels=10, value=0) -> Self:
1098    def pad(self, voxels=10, value=0) -> Self:
1099        """
1100        Add the specified number of voxels at the `Volume` borders.
1101        Voxels can be a list formatted as `[nx0, nx1, ny0, ny1, nz0, nz1]`.
1102
1103        Arguments:
1104            voxels : (int, list)
1105                number of voxels to be added (or a list of length 4)
1106            value : (int)
1107                intensity value (gray-scale color) of the padding
1108
1109        Example:
1110            ```python
1111            from vedo import Volume, dataurl, show
1112            iso = Volume(dataurl+'embryo.tif').isosurface()
1113            vol = iso.binarize(spacing=(100, 100, 100)).pad(10)
1114            vol.dilate([15,15,15])
1115            show(iso, vol.isosurface(), N=2, axes=1)
1116            ```
1117            ![](https://vedo.embl.es/images/volumetric/volume_pad.png)
1118        """
1119        x0, x1, y0, y1, z0, z1 = self.dataset.GetExtent()
1120        pf = vtki.new("ImageConstantPad")
1121        pf.SetInputData(self.dataset)
1122        pf.SetConstant(value)
1123        if utils.is_sequence(voxels):
1124            pf.SetOutputWholeExtent(
1125                x0 - voxels[0], x1 + voxels[1],
1126                y0 - voxels[2], y1 + voxels[3],
1127                z0 - voxels[4], z1 + voxels[5],
1128            )
1129        else:
1130            pf.SetOutputWholeExtent(
1131                x0 - voxels, x1 + voxels,
1132                y0 - voxels, y1 + voxels,
1133                z0 - voxels, z1 + voxels,
1134            )
1135        pf.Update()
1136        self._update(pf.GetOutput())
1137        self.pipeline = utils.OperationNode(
1138            "pad", comment=f"{voxels} voxels", parents=[self], c="#f28482"
1139        )
1140        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)

def resize(self, newdims: List[int]) -> Self:
1142    def resize(self, newdims: List[int]) -> Self:
1143        """Increase or reduce the number of voxels of a Volume with interpolation."""
1144        rsz = vtki.new("ImageResize")
1145        rsz.SetResizeMethodToOutputDimensions()
1146        rsz.SetInputData(self.dataset)
1147        rsz.SetOutputDimensions(newdims)
1148        rsz.Update()
1149        self.dataset = rsz.GetOutput()
1150        self._update(self.dataset)
1151        self.pipeline = utils.OperationNode(
1152            "resize", parents=[self], c="#4cc9f0", comment=f"dims={tuple(self.dimensions())}"
1153        )
1154        return self

Increase or reduce the number of voxels of a Volume with interpolation.

def normalize(self) -> Self:
1156    def normalize(self) -> Self:
1157        """Normalize that scalar components for each point."""
1158        norm = vtki.new("ImageNormalize")
1159        norm.SetInputData(self.dataset)
1160        norm.Update()
1161        self._update(norm.GetOutput())
1162        self.pipeline = utils.OperationNode("normalize", parents=[self], c="#4cc9f0")
1163        return self

Normalize that scalar components for each point.

def mirror(self, axis='x') -> Self:
1165    def mirror(self, axis="x") -> Self:
1166        """
1167        Mirror flip along one of the cartesian axes.
1168        """
1169        img = self.dataset
1170
1171        ff = vtki.new("ImageFlip")
1172        ff.SetInputData(img)
1173        if axis.lower() == "x":
1174            ff.SetFilteredAxis(0)
1175        elif axis.lower() == "y":
1176            ff.SetFilteredAxis(1)
1177        elif axis.lower() == "z":
1178            ff.SetFilteredAxis(2)
1179        else:
1180            vedo.logger.error("mirror must be set to either x, y, z or n")
1181            raise RuntimeError()
1182        ff.Update()
1183        self._update(ff.GetOutput())
1184        self.pipeline = utils.OperationNode(f"mirror {axis}", parents=[self], c="#4cc9f0")
1185        return self

Mirror flip along one of the cartesian axes.

def operation(self, operation: str, volume2=None) -> Volume:
1187    def operation(self, operation: str, volume2=None) -> "Volume":
1188        """
1189        Perform operations with `Volume` objects.
1190        Keyword `volume2` can be a constant `float`.
1191
1192        Possible operations are:
1193        ```
1194        and, or, xor, nand, nor, not,
1195        +, -, /, 1/x, sin, cos, exp, log,
1196        abs, **2, sqrt, min, max, atan, atan2, median,
1197        mag, dot, gradient, divergence, laplacian.
1198        ```
1199
1200        Example:
1201        ```py
1202        from vedo import Box, show
1203        vol1 = Box(size=(35,10, 5)).binarize()
1204        vol2 = Box(size=( 5,10,35)).binarize()
1205        vol = vol1.operation("xor", vol2)
1206        show([[vol1, vol2], 
1207            ["vol1 xor vol2", vol]],
1208            N=2, axes=1, viewup="z",
1209        ).close()
1210        ```
1211
1212        Note:
1213            For logic operations, the two volumes must have the same bounds.
1214            If they do not, a larger image is created to contain both and the
1215            volumes are resampled onto the larger image before the operation is
1216            performed. This can be slow and memory intensive.
1217
1218        See also:
1219            - [volume_operations.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/volume_operations.py)
1220        """
1221        op = operation.lower()
1222        image1 = self.dataset
1223
1224        if op in ["and", "or", "xor", "nand", "nor"]:
1225
1226            if not np.allclose(image1.GetBounds(), volume2.dataset.GetBounds()):
1227                # create a larger image to contain both
1228                b1 = image1.GetBounds()
1229                b2 = volume2.dataset.GetBounds()
1230                b = [
1231                    min(b1[0], b2[0]),
1232                    max(b1[1], b2[1]),
1233                    min(b1[2], b2[2]),
1234                    max(b1[3], b2[3]),
1235                    min(b1[4], b2[4]),
1236                    max(b1[5], b2[5]),
1237                ]
1238                dims1 = image1.GetDimensions()
1239                dims2 = volume2.dataset.GetDimensions()
1240                dims = [max(dims1[0], dims2[0]), max(dims1[1], dims2[1]), max(dims1[2], dims2[2])]
1241
1242                image = vtki.vtkImageData()
1243                image.SetDimensions(dims)
1244                spacing = (
1245                    (b[1] - b[0]) / dims[0],
1246                    (b[3] - b[2]) / dims[1],
1247                    (b[5] - b[4]) / dims[2],
1248                )
1249                image.SetSpacing(spacing)
1250                image.SetOrigin((b[0], b[2], b[4]))
1251                image.AllocateScalars(vtki.VTK_UNSIGNED_CHAR, 1)
1252                image.GetPointData().GetScalars().FillComponent(0, 0)
1253
1254                interp1 = vtki.new("ImageReslice")
1255                interp1.SetInputData(image1)
1256                interp1.SetOutputExtent(image.GetExtent())
1257                interp1.SetOutputOrigin(image.GetOrigin())
1258                interp1.SetOutputSpacing(image.GetSpacing())
1259                interp1.SetInterpolationModeToNearestNeighbor()
1260                interp1.Update()
1261                imageA = interp1.GetOutput()
1262
1263                interp2 = vtki.new("ImageReslice")
1264                interp2.SetInputData(volume2.dataset)
1265                interp2.SetOutputExtent(image.GetExtent())
1266                interp2.SetOutputOrigin(image.GetOrigin())
1267                interp2.SetOutputSpacing(image.GetSpacing())
1268                interp2.SetInterpolationModeToNearestNeighbor()
1269                interp2.Update()
1270                imageB = interp2.GetOutput()
1271
1272            else:
1273                imageA = image1
1274                imageB = volume2.dataset
1275
1276            img_logic = vtki.new("ImageLogic")
1277            img_logic.SetInput1Data(imageA)
1278            img_logic.SetInput2Data(imageB)
1279            img_logic.SetOperation(["and", "or", "xor", "nand", "nor"].index(op))
1280            img_logic.Update()
1281
1282            out_vol = Volume(img_logic.GetOutput())
1283            out_vol.pipeline = utils.OperationNode(
1284                "operation", comment=f"{op}", parents=[self, volume2], c="#4cc9f0", shape="cylinder"
1285            )
1286            return out_vol  ######################################################
1287
1288        if volume2 and isinstance(volume2, Volume):
1289            # assert image1.GetScalarType() == volume2.dataset.GetScalarType(), "volumes have different scalar types"
1290            # make sure they have the same bounds:
1291            assert np.allclose(image1.GetBounds(), volume2.dataset.GetBounds()), "volumes have different bounds"
1292            # make sure they have the same spacing:
1293            assert np.allclose(image1.GetSpacing(), volume2.dataset.GetSpacing()), "volumes have different spacing"
1294            # make sure they have the same origin:
1295            assert np.allclose(image1.GetOrigin(), volume2.dataset.GetOrigin()), "volumes have different origin"
1296
1297        mf = None
1298        if op in ["median"]:
1299            mf = vtki.new("ImageMedian3D")
1300            mf.SetInputData(image1)
1301        elif op in ["mag"]:
1302            mf = vtki.new("ImageMagnitude")
1303            mf.SetInputData(image1)
1304        elif op in ["dot"]:
1305            mf = vtki.new("ImageDotProduct")
1306            mf.SetInput1Data(image1)
1307            mf.SetInput2Data(volume2.dataset)
1308        elif op in ["grad", "gradient"]:
1309            mf = vtki.new("ImageGradient")
1310            mf.SetDimensionality(3)
1311            mf.SetInputData(image1)
1312        elif op in ["div", "divergence"]:
1313            mf = vtki.new("ImageDivergence")
1314            mf.SetInputData(image1)
1315        elif op in ["laplacian"]:
1316            mf = vtki.new("ImageLaplacian")
1317            mf.SetDimensionality(3)
1318            mf.SetInputData(image1)
1319        elif op in ["not"]:
1320            mf = vtki.new("ImageLogic")
1321            mf.SetInput1Data(image1)
1322            mf.SetOperation(4)
1323
1324        if mf is not None:
1325            mf.Update()
1326            vol = Volume(mf.GetOutput())
1327            vol.pipeline = utils.OperationNode(
1328                "operation", comment=f"{op}", parents=[self], c="#4cc9f0", shape="cylinder"
1329            )
1330            return vol  ######################################################
1331
1332        mat = vtki.new("ImageMathematics")
1333        mat.SetInput1Data(image1)
1334
1335        K = None
1336
1337        if utils.is_number(volume2):
1338            K = volume2
1339            mat.SetConstantK(K)
1340            mat.SetConstantC(K)
1341
1342        elif volume2 is not None:  # assume image2 is a constant value
1343            mat.SetInput2Data(volume2.dataset)
1344
1345        # ###########################
1346        if op in ["+", "add", "plus"]:
1347            if K:
1348                mat.SetOperationToAddConstant()
1349            else:
1350                mat.SetOperationToAdd()
1351
1352        elif op in ["-", "subtract", "minus"]:
1353            if K:
1354                mat.SetConstantC(-float(K))
1355                mat.SetOperationToAddConstant()
1356            else:
1357                mat.SetOperationToSubtract()
1358
1359        elif op in ["*", "multiply", "times"]:
1360            if K:
1361                mat.SetOperationToMultiplyByK()
1362            else:
1363                mat.SetOperationToMultiply()
1364
1365        elif op in ["/", "divide"]:
1366            if K:
1367                mat.SetConstantK(1.0 / K)
1368                mat.SetOperationToMultiplyByK()
1369            else:
1370                mat.SetOperationToDivide()
1371
1372        elif op in ["1/x", "invert"]:
1373            mat.SetOperationToInvert()
1374        elif op in ["sin"]:
1375            mat.SetOperationToSin()
1376        elif op in ["cos"]:
1377            mat.SetOperationToCos()
1378        elif op in ["exp"]:
1379            mat.SetOperationToExp()
1380        elif op in ["log"]:
1381            mat.SetOperationToLog()
1382        elif op in ["abs"]:
1383            mat.SetOperationToAbsoluteValue()
1384        elif op in ["**2", "square"]:
1385            mat.SetOperationToSquare()
1386        elif op in ["sqrt", "sqr"]:
1387            mat.SetOperationToSquareRoot()
1388        elif op in ["min"]:
1389            mat.SetOperationToMin()
1390        elif op in ["max"]:
1391            mat.SetOperationToMax()
1392        elif op in ["atan"]:
1393            mat.SetOperationToATAN()
1394        elif op in ["atan2"]:
1395            mat.SetOperationToATAN2()
1396        else:
1397            vedo.logger.error(f"unknown operation {operation}")
1398            raise RuntimeError()
1399        mat.Update()
1400
1401        self._update(mat.GetOutput())
1402
1403        self.pipeline = utils.OperationNode(
1404            "operation", comment=f"{op}", parents=[self, volume2], shape="cylinder", c="#4cc9f0"
1405        )
1406        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:
def frequency_pass_filter(self, low_cutoff=None, high_cutoff=None, order=1) -> Self:
1408    def frequency_pass_filter(self, low_cutoff=None, high_cutoff=None, order=1) -> Self:
1409        """
1410        Low-pass and high-pass filtering become trivial in the frequency domain.
1411        A portion of the pixels/voxels are simply masked or attenuated.
1412        This function applies a high pass Butterworth filter that attenuates the
1413        frequency domain image.
1414
1415        The gradual attenuation of the filter is important.
1416        A simple high-pass filter would simply mask a set of pixels in the frequency domain,
1417        but the abrupt transition would cause a ringing effect in the spatial domain.
1418
1419        Arguments:
1420            low_cutoff : (list)
1421                the cutoff frequencies for x, y and z
1422            high_cutoff : (list)
1423                the cutoff frequencies for x, y and z
1424            order : (int)
1425                order determines sharpness of the cutoff curve
1426        """
1427        # https://lorensen.github.io/VTKExamples/site/Cxx/ImageProcessing/IdealHighPass
1428        fft = vtki.new("ImageFFT")
1429        fft.SetInputData(self.dataset)
1430        fft.Update()
1431        out = fft.GetOutput()
1432
1433        if high_cutoff:
1434            blp = vtki.new("ImageButterworthLowPass")
1435            blp.SetInputData(out)
1436            blp.SetCutOff(high_cutoff)
1437            blp.SetOrder(order)
1438            blp.Update()
1439            out = blp.GetOutput()
1440
1441        if low_cutoff:
1442            bhp = vtki.new("ImageButterworthHighPass")
1443            bhp.SetInputData(out)
1444            bhp.SetCutOff(low_cutoff)
1445            bhp.SetOrder(order)
1446            bhp.Update()
1447            out = bhp.GetOutput()
1448
1449        rfft = vtki.new("ImageRFFT")
1450        rfft.SetInputData(out)
1451        rfft.Update()
1452
1453        ecomp = vtki.new("ImageExtractComponents")
1454        ecomp.SetInputData(rfft.GetOutput())
1455        ecomp.SetComponents(0)
1456        ecomp.Update()
1457        self._update(ecomp.GetOutput())
1458        self.pipeline = utils.OperationNode("frequency_pass_filter", parents=[self], c="#4cc9f0")
1459        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
def smooth_gaussian(self, sigma=(2, 2, 2), radius=None) -> Self:
1461    def smooth_gaussian(self, sigma=(2, 2, 2), radius=None) -> Self:
1462        """
1463        Performs a convolution of the input Volume with a gaussian.
1464
1465        Arguments:
1466            sigma : (float, list)
1467                standard deviation(s) in voxel units.
1468                A list can be given to smooth in the three direction differently.
1469            radius : (float, list)
1470                radius factor(s) determine how far out the gaussian
1471                kernel will go before being clamped to zero. A list can be given too.
1472        """
1473        gsf = vtki.new("ImageGaussianSmooth")
1474        gsf.SetDimensionality(3)
1475        gsf.SetInputData(self.dataset)
1476        if utils.is_sequence(sigma):
1477            gsf.SetStandardDeviations(sigma)
1478        else:
1479            gsf.SetStandardDeviation(sigma)
1480        if radius is not None:
1481            if utils.is_sequence(radius):
1482                gsf.SetRadiusFactors(radius)
1483            else:
1484                gsf.SetRadiusFactor(radius)
1485        gsf.Update()
1486        self._update(gsf.GetOutput())
1487        self.pipeline = utils.OperationNode("smooth_gaussian", parents=[self], c="#4cc9f0")
1488        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.
def smooth_median(self, neighbours=(2, 2, 2)) -> Self:
1490    def smooth_median(self, neighbours=(2, 2, 2)) -> Self:
1491        """
1492        Median filter that replaces each pixel with the median value
1493        from a rectangular neighborhood around that pixel.
1494        """
1495        imgm = vtki.new("ImageMedian3D")
1496        imgm.SetInputData(self.dataset)
1497        if utils.is_sequence(neighbours):
1498            imgm.SetKernelSize(neighbours[0], neighbours[1], neighbours[2])
1499        else:
1500            imgm.SetKernelSize(neighbours, neighbours, neighbours)
1501        imgm.Update()
1502        self._update(imgm.GetOutput())
1503        self.pipeline = utils.OperationNode("smooth_median", parents=[self], c="#4cc9f0")
1504        return self

Median filter that replaces each pixel with the median value from a rectangular neighborhood around that pixel.

def erode(self, neighbours=(2, 2, 2)) -> Self:
1506    def erode(self, neighbours=(2, 2, 2)) -> Self:
1507        """
1508        Replace a voxel with the minimum over an ellipsoidal neighborhood of voxels.
1509        If `neighbours` of an axis is 1, no processing is done on that axis.
1510
1511        Examples:
1512            - [erode_dilate.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/erode_dilate.py)
1513
1514                ![](https://vedo.embl.es/images/volumetric/erode_dilate.png)
1515        """
1516        ver = vtki.new("ImageContinuousErode3D")
1517        ver.SetInputData(self.dataset)
1518        ver.SetKernelSize(neighbours[0], neighbours[1], neighbours[2])
1519        ver.Update()
1520        self._update(ver.GetOutput())
1521        self.pipeline = utils.OperationNode("erode", parents=[self], c="#4cc9f0")
1522        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:
def dilate(self, neighbours=(2, 2, 2)) -> Self:
1524    def dilate(self, neighbours=(2, 2, 2)) -> Self:
1525        """
1526        Replace a voxel with the maximum over an ellipsoidal neighborhood of voxels.
1527        If `neighbours` of an axis is 1, no processing is done on that axis.
1528
1529        Check also `erode()` and `pad()`.
1530
1531        Examples:
1532            - [erode_dilate.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/erode_dilate.py)
1533        """
1534        ver = vtki.new("ImageContinuousDilate3D")
1535        ver.SetInputData(self.dataset)
1536        ver.SetKernelSize(neighbours[0], neighbours[1], neighbours[2])
1537        ver.Update()
1538        self._update(ver.GetOutput())
1539        self.pipeline = utils.OperationNode("dilate", parents=[self], c="#4cc9f0")
1540        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.

Check also erode() and pad().

Examples:
def magnitude(self) -> Self:
1542    def magnitude(self) -> Self:
1543        """Colapses components with magnitude function."""
1544        imgm = vtki.new("ImageMagnitude")
1545        imgm.SetInputData(self.dataset)
1546        imgm.Update()
1547        self._update(imgm.GetOutput())
1548        self.pipeline = utils.OperationNode("magnitude", parents=[self], c="#4cc9f0")
1549        return self

Colapses components with magnitude function.

def topoints(self) -> vedo.pointcloud.Points:
1551    def topoints(self) -> "vedo.Points":
1552        """
1553        Extract all image voxels as points.
1554        This function takes an input `Volume` and creates an `Mesh`
1555        that contains the points and the point attributes.
1556
1557        Examples:
1558            - [vol2points.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/vol2points.py)
1559        """
1560        v2p = vtki.new("ImageToPoints")
1561        v2p.SetInputData(self.dataset)
1562        v2p.Update()
1563        mpts = vedo.Points(v2p.GetOutput())
1564        mpts.pipeline = utils.OperationNode("topoints", parents=[self], c="#4cc9f0:#e9c46a")
1565        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:
def euclidean_distance(self, anisotropy=False, max_distance=None) -> Volume:
1567    def euclidean_distance(self, anisotropy=False, max_distance=None) -> "Volume":
1568        """
1569        Implementation of the Euclidean DT (Distance Transform) using Saito's algorithm.
1570        The distance map produced contains the square of the Euclidean distance values.
1571        The algorithm has a O(n^(D+1)) complexity over n x n x...x n images in D dimensions.
1572
1573        Check out also: https://en.wikipedia.org/wiki/Distance_transform
1574
1575        Arguments:
1576            anisotropy : bool
1577                used to define whether Spacing should be used in the
1578                computation of the distances.
1579            max_distance : bool
1580                any distance bigger than max_distance will not be
1581                computed but set to this specified value instead.
1582
1583        Examples:
1584            - [euclidian_dist.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/euclidian_dist.py)
1585        """
1586        euv = vtki.new("ImageEuclideanDistance")
1587        euv.SetInputData(self.dataset)
1588        euv.SetConsiderAnisotropy(anisotropy)
1589        if max_distance is not None:
1590            euv.InitializeOn()
1591            euv.SetMaximumDistance(max_distance)
1592        euv.SetAlgorithmToSaito()
1593        euv.Update()
1594        vol = Volume(euv.GetOutput())
1595        vol.pipeline = utils.OperationNode("euclidean_distance", parents=[self], c="#4cc9f0")
1596        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:
def correlation_with(self, vol2: Volume, dim=2) -> Volume:
1598    def correlation_with(self, vol2: "Volume", dim=2) -> "Volume":
1599        """
1600        Find the correlation between two volumetric data sets.
1601        Keyword `dim` determines whether the correlation will be 3D, 2D or 1D.
1602        The default is a 2D Correlation.
1603
1604        The output size will match the size of the first input.
1605        The second input is considered the correlation kernel.
1606        """
1607        imc = vtki.new("ImageCorrelation")
1608        imc.SetInput1Data(self.dataset)
1609        imc.SetInput2Data(vol2.dataset)
1610        imc.SetDimensionality(dim)
1611        imc.Update()
1612        vol = Volume(imc.GetOutput())
1613
1614        vol.pipeline = utils.OperationNode("correlation_with", parents=[self, vol2], c="#4cc9f0")
1615        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.

def scale_voxels(self, scale=1) -> Self:
1617    def scale_voxels(self, scale=1) -> Self:
1618        """Scale the voxel content by factor `scale`."""
1619        rsl = vtki.new("ImageReslice")
1620        rsl.SetInputData(self.dataset)
1621        rsl.SetScalarScale(scale)
1622        rsl.Update()
1623        self._update(rsl.GetOutput())
1624        self.pipeline = utils.OperationNode(
1625            "scale_voxels", comment=f"scale={scale}", parents=[self], c="#4cc9f0"
1626        )
1627        return self

Scale the voxel content by factor scale.