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

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

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                # make asigmoidal transfer function by default
195                # xvalues = np.linspace(0, 1, 11)
196                # sigmoid = np.clip(1/(1+np.exp(-20*(xvalues-0.5))), 0, 1)
197                # print("Volume: setting sigmoidal transfer function", xvalues, sigmoid)
198                # self.alpha(sigmoid)
199                self.alpha([0.0, 0.001, 0.3, 0.5, 0.7, 0.8, 1.0]) # we need to revert this..
200                self.alpha_gradient(None)
201                self.properties.SetScalarOpacityUnitDistance(1.0)
202
203        self.pipeline = utils.OperationNode(
204            "Volume", comment=f"dims={tuple(self.dimensions())}", c="#4cc9f0"
205        )
206        #######################################################################

This class can be initialized with a numpy object, a vtkImageData or a list of 2D bmp files.

Arguments:
  • input_obj : (str, vtkImageData, np.ndarray) input data can be a file name, a vtkImageData or a numpy object.
  • origin : (list) set volume origin coordinates
  • spacing : (list) voxel dimensions in x, y and z.
  • dims : (list) specify the dimensions of the volume.
Note:

If your input is an array ordered as ZYX you can permute it to XYZ with: array = np.transpose(array, axes=[2, 1, 0]). Alternatively you can also use 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
208    @property
209    def mapper(self):
210        """Return the underlying `vtkMapper` object."""
211        return self.actor.GetMapper()

Return the underlying vtkMapper object.

def c(self, *args, **kwargs) -> Self:
242    def c(self, *args, **kwargs) -> Self:
243        """Deprecated. Use `Volume.cmap()` instead."""
244        vedo.logger.warning("Volume.c() is deprecated, use Volume.cmap() instead")
245        return self.cmap(*args, **kwargs)

Deprecated. Use Volume.cmap() instead.

def copy(self, deep=True) -> Volume:
384    def copy(self, deep=True) -> "Volume":
385        """Return a copy of the Volume. Alias of `clone()`."""
386        return self.clone(deep=deep)

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

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

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

def astype(self, dtype: Union[str, int]) -> Self:
406    def astype(self, dtype: Union[str, int]) -> Self:
407        """
408        Reset the type of the scalars array.
409
410        Arguments:
411            dtype : (str)
412                the type of the scalars array in
413                ["int8", "uint8", "int16", "uint16", "int32", "uint32", "float32", "float64"]
414        """
415        if dtype in ["int8", "uint8", "int16", "uint16", "int32", "uint32", "float32", "float64"]:
416            caster = vtki.new("ImageCast")
417            caster.SetInputData(self.dataset)
418            caster.SetOutputScalarType(int(vtki.array_types[dtype]))
419            caster.ClampOverflowOn()
420            caster.Update()
421            self._update(caster.GetOutput())
422            self.pipeline = utils.OperationNode(f"astype({dtype})", parents=[self], c="#4cc9f0")
423        else:
424            vedo.logger.error(f"astype(): unknown type {dtype}")
425            raise ValueError()
426        return self

Reset the type of the scalars array.

Arguments:
  • dtype : (str) the type of the scalars array in ["int8", "uint8", "int16", "uint16", "int32", "uint32", "float32", "float64"]
def component_weight(self, i: int, weight: float) -> Self:
428    def component_weight(self, i: int, weight: float) -> Self:
429        """Set the scalar component weight in range [0,1]."""
430        self.properties.SetComponentWeight(i, weight)
431        return self

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

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

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

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

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

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

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

def slice_plane( self, origin: List[float], normal: List[float], autocrop=False, border=0.5, mode='linear') -> vedo.mesh.Mesh:
472    def slice_plane(self, origin: List[float], normal: List[float], autocrop=False, border=0.5, mode="linear") -> Mesh:
473        """
474        Extract the slice along a given plane position and normal.
475
476        Two metadata arrays are added to the output Mesh:
477            - "shape" : contains the shape of the slice
478            - "original_bounds" : contains the original bounds of the slice
479        One can access them with e.g. `myslice.metadata["shape"]`.
480
481        Arguments:
482            origin : (list)
483                position of the plane
484            normal : (list)
485                normal to the plane
486            autocrop : (bool)
487                crop the output to the minimal possible size
488            border : (float)
489                add a border to the output slice
490            mode : (str)
491                interpolation mode, one of the following: "linear", "nearest", "cubic"
492
493        Example:
494            - [slice_plane1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slice_plane1.py)
495
496                ![](https://vedo.embl.es/images/volumetric/slicePlane1.gif)
497            
498            - [slice_plane2.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slice_plane2.py)
499                
500                ![](https://vedo.embl.es/images/volumetric/slicePlane2.png)
501
502            - [slice_plane3.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slice_plane3.py)
503
504                ![](https://vedo.embl.es/images/volumetric/slicePlane3.jpg)
505        """
506        newaxis = utils.versor(normal)
507        pos = np.array(origin)
508        initaxis = (0, 0, 1)
509        crossvec = np.cross(initaxis, newaxis)
510        angle = np.arccos(np.dot(initaxis, newaxis))
511        T = vtki.vtkTransform()
512        T.PostMultiply()
513        T.RotateWXYZ(np.rad2deg(angle), crossvec.tolist())
514        T.Translate(pos.tolist())
515
516        reslice = vtki.new("ImageReslice")
517        reslice.SetResliceAxes(T.GetMatrix())
518        reslice.SetInputData(self.dataset)
519        reslice.SetOutputDimensionality(2)
520        reslice.SetTransformInputSampling(True)
521        reslice.SetGenerateStencilOutput(False)
522        if border:
523            reslice.SetBorder(True)
524            reslice.SetBorderThickness(border)
525        else:
526            reslice.SetBorder(False)
527        if mode == "linear":
528            reslice.SetInterpolationModeToLinear()
529        elif mode == "nearest":
530            reslice.SetInterpolationModeToNearestNeighbor()
531        elif mode == "cubic":
532            reslice.SetInterpolationModeToCubic()
533        else:
534            vedo.logger.error(f"in slice_plane(): unknown interpolation mode {mode}")
535            raise ValueError()
536        reslice.SetAutoCropOutput(not autocrop)
537        reslice.Update()
538        img = reslice.GetOutput()
539
540        vslice = vtki.new("ImageDataGeometryFilter")
541        vslice.SetInputData(img)
542        vslice.Update()
543
544        msh = Mesh(vslice.GetOutput()).apply_transform(T)
545        msh.properties.LightingOff()
546
547        d0, d1, _ = img.GetDimensions()
548        varr1 = utils.numpy2vtk([d1, d0], name="shape")
549        varr2 = utils.numpy2vtk(img.GetBounds(), name="original_bounds")
550        msh.dataset.GetFieldData().AddArray(varr1)
551        msh.dataset.GetFieldData().AddArray(varr2)
552        msh.pipeline = utils.OperationNode("slice_plane", parents=[self], c="#4cc9f0:#e9c46a")
553        return msh

Extract the slice along a given plane position and normal.

Two metadata arrays are added to the output Mesh:
  • "shape" : contains the shape of the slice
  • "original_bounds" : contains the original bounds of the slice

One can access them with e.g. myslice.metadata["shape"].

Arguments:
  • origin : (list) position of the plane
  • normal : (list) normal to the plane
  • autocrop : (bool) crop the output to the minimal possible size
  • border : (float) add a border to the output slice
  • mode : (str) interpolation mode, one of the following: "linear", "nearest", "cubic"
Example:
def slab(self, slice_range=(), axis='z', operation='mean') -> vedo.mesh.Mesh:
555    def slab(self, slice_range=(), axis='z', operation="mean") -> Mesh:
556        """
557        Extract a slab from a `Volume` by combining 
558        all of the slices of an image to create a single slice.
559
560        Returns a `Mesh` containing metadata which
561        can be accessed with e.g. `mesh.metadata["slab_range"]`.
562
563        Metadata:
564            slab_range : (list)
565                contains the range of slices extracted
566            slab_axis : (str)
567                contains the axis along which the slab was extracted
568            slab_operation : (str)
569                contains the operation performed on the slab
570            slab_bounding_box : (list)
571                contains the bounding box of the slab
572
573        Arguments:
574            slice_range : (list)
575                range of slices to extract
576            axis : (str)
577                axis along which to extract the slab
578            operation : (str)
579                operation to perform on the slab,
580                allowed values are: "sum", "min", "max", "mean".
581        
582        Example:
583            - [slab.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/slab_vol.py)
584
585            ![](https://vedo.embl.es/images/volumetric/slab_vol.jpg)
586        """
587        if len(slice_range) != 2:
588            vedo.logger.error("in slab(): slice_range is empty or invalid")
589            raise ValueError()
590        
591        islab = vtki.new("ImageSlab")
592        islab.SetInputData(self.dataset)
593
594        if operation in ["+", "add", "sum"]:
595            islab.SetOperationToSum()
596        elif "min" in operation:
597            islab.SetOperationToMin()
598        elif "max" in operation:
599            islab.SetOperationToMax()
600        elif "mean" in operation:
601            islab.SetOperationToMean()
602        else:
603            vedo.logger.error(f"in slab(): unknown operation {operation}")
604            raise ValueError()
605
606        dims = self.dimensions()
607        if axis == 'x':
608            islab.SetOrientationToX()
609            if slice_range[0]  > dims[0]-1:
610                slice_range[0] = dims[0]-1
611            if slice_range[1]  > dims[0]-1:
612                slice_range[1] = dims[0]-1
613        elif axis == 'y':
614            islab.SetOrientationToY()
615            if slice_range[0]  > dims[1]-1:
616                slice_range[0] = dims[1]-1
617            if slice_range[1]  > dims[1]-1:
618                slice_range[1] = dims[1]-1
619        elif axis == 'z':
620            islab.SetOrientationToZ()
621            if slice_range[0]  > dims[2]-1:
622                slice_range[0] = dims[2]-1
623            if slice_range[1]  > dims[2]-1:
624                slice_range[1] = dims[2]-1
625        else:
626            vedo.logger.error(f"Error in slab(): unknown axis {axis}")
627            raise RuntimeError()
628        
629        islab.SetSliceRange(slice_range)
630        islab.Update()
631
632        msh = Mesh(islab.GetOutput()).lighting('off')
633        msh.mapper.SetLookupTable(utils.ctf2lut(self, msh))
634        msh.mapper.SetScalarRange(self.scalar_range())
635
636        msh.metadata["slab_range"] = slice_range
637        msh.metadata["slab_axis"]  = axis
638        msh.metadata["slab_operation"] = operation
639
640        # compute bounds of slab
641        origin = list(self.origin())
642        spacing = list(self.spacing())
643        if axis == 'x':
644            msh.metadata["slab_bounding_box"] = [
645                origin[0] + slice_range[0]*spacing[0],
646                origin[0] + slice_range[1]*spacing[0],
647                origin[1],
648                origin[1] + dims[1]*spacing[1],
649                origin[2],
650                origin[2] + dims[2]*spacing[2],
651            ]
652        elif axis == 'y':
653            msh.metadata["slab_bounding_box"] = [
654                origin[0],
655                origin[0] + dims[0]*spacing[0],
656                origin[1] + slice_range[0]*spacing[1],
657                origin[1] + slice_range[1]*spacing[1],
658                origin[2],
659                origin[2] + dims[2]*spacing[2],
660            ]
661        elif axis == 'z':
662            msh.metadata["slab_bounding_box"] = [
663                origin[0],
664                origin[0] + dims[0]*spacing[0],
665                origin[1],
666                origin[1] + dims[1]*spacing[1],
667                origin[2] + slice_range[0]*spacing[2],
668                origin[2] + slice_range[1]*spacing[2],
669            ]
670
671        msh.pipeline = utils.OperationNode(
672            f"slab{slice_range}", 
673            comment=f"axis={axis}, operation={operation}",
674            parents=[self],
675            c="#4cc9f0:#e9c46a",
676        )
677        msh.name = "SlabMesh"
678        return msh

Extract a slab from a Volume by combining all of the slices of an image to create a single slice.

Returns a Mesh containing metadata which can be accessed with e.g. mesh.metadata["slab_range"].

Metadata:

slab_range : (list) contains the range of slices extracted slab_axis : (str) contains the axis along which the slab was extracted slab_operation : (str) contains the operation performed on the slab slab_bounding_box : (list) contains the bounding box of the slab

Arguments:
  • slice_range : (list) range of slices to extract
  • axis : (str) axis along which to extract the slab
  • operation : (str) operation to perform on the slab, allowed values are: "sum", "min", "max", "mean".
Example:

def warp( self, source: Union[vedo.pointcloud.Points, List], target: Union[vedo.pointcloud.Points, List], sigma=1, mode='3d', fit=True) -> Self:
681    def warp(
682            self,
683            source: Union["vedo.Points", List],
684            target: Union["vedo.Points", List],
685            sigma=1, mode="3d", fit=True,
686        ) -> Self:
687        """
688        Warp volume scalars within a Volume by specifying
689        source and target sets of points.
690
691        Arguments:
692            source : (Points, list)
693                the list of source points
694            target : (Points, list)
695                the list of target points
696            fit : (bool)
697                fit/adapt the old bounding box to the warped geometry
698        """
699        if isinstance(source, vedo.Points):
700            source = source.coordinates
701        if isinstance(target, vedo.Points):
702            target = target.coordinates
703
704        NLT = transformations.NonLinearTransform()
705        NLT.source_points = source
706        NLT.target_points = target
707        NLT.sigma = sigma
708        NLT.mode = mode
709
710        self.apply_transform(NLT, fit=fit)
711        self.pipeline = utils.OperationNode("warp", parents=[self], c="#4cc9f0")
712        return self

Warp volume scalars within a Volume by specifying source and target sets of points.

Arguments:
  • source : (Points, list) the list of source points
  • target : (Points, list) the list of target points
  • fit : (bool) fit/adapt the old bounding box to the warped geometry
def apply_transform( self, T: Union[vedo.transformations.LinearTransform, vedo.transformations.NonLinearTransform], fit=True, interpolation='cubic') -> Self:
714    def apply_transform(
715            self, 
716            T: Union[transformations.LinearTransform, transformations.NonLinearTransform],
717            fit=True, interpolation="cubic",
718        ) -> Self:
719        """
720        Apply a transform to the scalars in the volume.
721
722        Arguments:
723            T : (LinearTransform, NonLinearTransform)
724                The transformation to be applied
725            fit : (bool)
726                fit/adapt the old bounding box to the modified geometry
727            interpolation : (str)
728                one of the following: "nearest", "linear", "cubic"
729        """
730        if utils.is_sequence(T):
731            T = transformations.LinearTransform(T)
732
733        TI = T.compute_inverse()
734
735        reslice = vtki.new("ImageReslice")
736        reslice.SetInputData(self.dataset)
737        reslice.SetResliceTransform(TI.T)
738        reslice.SetOutputDimensionality(3)
739        if "lin" in interpolation.lower():
740            reslice.SetInterpolationModeToLinear()
741        elif "near" in interpolation.lower():
742            reslice.SetInterpolationModeToNearestNeighbor()
743        elif "cubic" in interpolation.lower():
744            reslice.SetInterpolationModeToCubic()
745        else:
746            vedo.logger.error(
747                f"in apply_transform: unknown interpolation mode {interpolation}")
748            raise ValueError()
749        reslice.SetAutoCropOutput(fit)
750        reslice.Update()
751        self._update(reslice.GetOutput())
752        self.transform = T
753        self.pipeline = utils.OperationNode(
754            "apply_transform", parents=[self], c="#4cc9f0")
755        return self

Apply a transform to the scalars in the volume.

Arguments:
  • T : (LinearTransform, NonLinearTransform) The transformation to be applied
  • fit : (bool) fit/adapt the old bounding box to the modified geometry
  • interpolation : (str) one of the following: "nearest", "linear", "cubic"
def imagedata(self) -> vtkmodules.vtkCommonDataModel.vtkImageData:
757    def imagedata(self) -> vtki.vtkImageData:
758        """
759        DEPRECATED:
760        Use `Volume.dataset` instead.
761
762        Return the underlying `vtkImagaData` object.
763        """
764        print("Volume.imagedata() is deprecated, use Volume.dataset instead")
765        return self.dataset

DEPRECATED: Use Volume.dataset instead.

Return the underlying vtkImagaData object.

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

Mark the object as modified.

Example:

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

Get read-write access to voxels of a Volume object as a numpy array.

When you set values in the output image, you don't want numpy to reallocate the array but instead set values in the existing array, so use the [:] operator.

Example:

arr[:] = arr*2 + 15

If the array is modified add a call to: volume.modified() when all your modifications are completed.

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

Return the nr. of voxels in the 3 dimensions.

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

Return the nr. of voxels in the 3 dimensions.

def scalar_range(self) -> numpy.ndarray:
818    def scalar_range(self) -> np.ndarray:
819        """Return the range of the scalar values."""
820        return np.array(self.dataset.GetScalarRange())

Return the range of the scalar values.

def spacing(self, s=None) -> Union[Self, Iterable[float]]:
822    def spacing(self, s=None) -> Union[Self, Iterable[float]]:
823        """Set/get the voxels size in the 3 dimensions."""
824        if s is not None:
825            self.dataset.SetSpacing(s)
826            return self
827        return np.array(self.dataset.GetSpacing())

Set/get the voxels size in the 3 dimensions.

def origin(self, s=None) -> Union[Self, Iterable[float]]:
829    def origin(self, s=None) -> Union[Self, Iterable[float]]:
830        """
831        Set/get the origin of the volumetric dataset.
832
833        The origin is the position in world coordinates of the point index (0,0,0).
834        This point does not have to be part of the dataset, in other words,
835        the dataset extent does not have to start at (0,0,0) and the origin 
836        can be outside of the dataset bounding box. 
837        The origin plus spacing determine the position in space of the points.
838        """
839        if s is not None:
840            self.dataset.SetOrigin(s)
841            return self
842        return np.array(self.dataset.GetOrigin())

Set/get the origin of the volumetric dataset.

The origin is the position in world coordinates of the point index (0,0,0). This point does not have to be part of the dataset, in other words, the dataset extent does not have to start at (0,0,0) and the origin can be outside of the dataset bounding box. The origin plus spacing determine the position in space of the points.

def pos(self, p=None) -> Union[Self, Iterable[float]]:
844    def pos(self, p=None) -> Union[Self, Iterable[float]]:
845        """Set/get the position of the volumetric dataset."""
846        if p is not None:
847            self.origin(p)
848            return self
849        return self.origin()

Set/get the position of the volumetric dataset.

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

Get the center of the volumetric dataset.

def shift(self, s: list) -> Self:
856    def shift(self, s: list) -> Self:
857        """Shift the volumetric dataset by a vector."""
858        self.origin(self.origin() + np.array(s))
859        return self

Shift the volumetric dataset by a vector.

def rotate_x(self, angle: float, rad=False, around=None) -> Self:
861    def rotate_x(self, angle: float, rad=False, around=None) -> Self:
862        """
863        Rotate around x-axis. If angle is in radians set `rad=True`.
864
865        Use `around` to define a pivoting point.
866        """
867        if angle == 0:
868            return self
869        LT = transformations.LinearTransform().rotate_x(angle, rad, around)
870        return self.apply_transform(LT, fit=True, interpolation="linear")

Rotate around x-axis. If angle is in radians set rad=True.

Use around to define a pivoting point.

def rotate_y(self, angle: float, rad=False, around=None) -> Self:
872    def rotate_y(self, angle: float, rad=False, around=None) -> Self:
873        """
874        Rotate around y-axis. If angle is in radians set `rad=True`.
875
876        Use `around` to define a pivoting point.
877        """
878        if angle == 0:
879            return self
880        LT = transformations.LinearTransform().rotate_y(angle, rad, around)
881        return self.apply_transform(LT, fit=True, interpolation="linear")

Rotate around y-axis. If angle is in radians set rad=True.

Use around to define a pivoting point.

def rotate_z(self, angle: float, rad=False, around=None) -> Self:
883    def rotate_z(self, angle: float, rad=False, around=None) -> Self:
884        """
885        Rotate around z-axis. If angle is in radians set `rad=True`.
886
887        Use `around` to define a pivoting point.
888        """
889        if angle == 0:
890            return self
891        LT = transformations.LinearTransform().rotate_z(angle, rad, around)
892        return self.apply_transform(LT, fit=True, interpolation="linear")

Rotate around z-axis. If angle is in radians set rad=True.

Use around to define a pivoting point.

def get_cell_from_ijk(self, ijk: list) -> int:
894    def get_cell_from_ijk(self, ijk: list) -> int:
895        """
896        Get the voxel id number at the given ijk coordinates.
897
898        Arguments:
899            ijk : (list)
900                the ijk coordinates of the voxel
901        """
902        return self.dataset.ComputeCellId(ijk)

Get the voxel id number at the given ijk coordinates.

Arguments:
  • ijk : (list) the ijk coordinates of the voxel
def get_point_from_ijk(self, ijk: list) -> int:
904    def get_point_from_ijk(self, ijk: list) -> int:
905        """
906        Get the point id number at the given ijk coordinates.
907
908        Arguments:
909            ijk : (list)
910                the ijk coordinates of the voxel
911        """
912        return self.dataset.ComputePointId(ijk)

Get the point id number at the given ijk coordinates.

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

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

def resample(self, new_spacing: List[float], interpolation=1) -> Self:
929    def resample(self, new_spacing: List[float], interpolation=1) -> Self:
930        """
931        Resamples a `Volume` to be larger or smaller.
932
933        This method modifies the spacing of the input.
934        Linear interpolation is used to resample the data.
935
936        Arguments:
937            new_spacing : (list)
938                a list of 3 new spacings for the 3 axes
939            interpolation : (int)
940                0=nearest_neighbor, 1=linear, 2=cubic
941        """
942        rsp = vtki.new("ImageResample")
943        oldsp = self.spacing()
944        for i in range(3):
945            if oldsp[i] != new_spacing[i]:
946                rsp.SetAxisOutputSpacing(i, new_spacing[i])
947        rsp.InterpolateOn()
948        rsp.SetInterpolationMode(interpolation)
949        rsp.OptimizationOn()
950        rsp.Update()
951        self._update(rsp.GetOutput())
952        self.pipeline = utils.OperationNode(
953            "resample", comment=f"spacing: {tuple(new_spacing)}", parents=[self], c="#4cc9f0"
954        )
955        return self

Resamples a Volume to be larger or smaller.

This method modifies the spacing of the input. Linear interpolation is used to resample the data.

Arguments:
  • new_spacing : (list) a list of 3 new spacings for the 3 axes
  • interpolation : (int) 0=nearest_neighbor, 1=linear, 2=cubic
def threshold(self, above=None, below=None, replace=None, replace_value=None) -> Self:
 958    def threshold(self, above=None, below=None, replace=None, replace_value=None) -> Self:
 959        """
 960        Binary or continuous volume thresholding.
 961        Find the voxels that contain a value above/below the input values
 962        and replace them with a new value (default is 0).
 963        """
 964        th = vtki.new("ImageThreshold")
 965        th.SetInputData(self.dataset)
 966
 967        # sanity checks
 968        if above is not None and below is not None:
 969            if above == below:
 970                return self
 971            if above > below:
 972                vedo.logger.warning("in volume.threshold(), above > below, skip.")
 973                return self
 974
 975        ## cases
 976        if below is not None and above is not None:
 977            th.ThresholdBetween(above, below)
 978
 979        elif above is not None:
 980            th.ThresholdByUpper(above)
 981
 982        elif below is not None:
 983            th.ThresholdByLower(below)
 984
 985        ##
 986        if replace is not None:
 987            th.SetReplaceIn(True)
 988            th.SetInValue(replace)
 989        else:
 990            th.SetReplaceIn(False)
 991
 992        if replace_value is not None:
 993            th.SetReplaceOut(True)
 994            th.SetOutValue(replace_value)
 995        else:
 996            th.SetReplaceOut(False)
 997
 998        th.Update()
 999        self._update(th.GetOutput())
1000        self.pipeline = utils.OperationNode("threshold", parents=[self], c="#4cc9f0")
1001        return self

Binary or continuous volume thresholding. Find the voxels that contain a value above/below the input values and replace them with a new value (default is 0).

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

Crop a Volume object.

Arguments:
  • left : (float) fraction to crop from the left plane (negative x)
  • right : (float) fraction to crop from the right plane (positive x)
  • back : (float) fraction to crop from the back plane (negative y)
  • front : (float) fraction to crop from the front plane (positive y)
  • bottom : (float) fraction to crop from the bottom plane (negative z)
  • top : (float) fraction to crop from the top plane (positive z)
  • VOI : (list) extract Volume Of Interest expressed in voxel numbers
Example:

vol.crop(VOI=(xmin, xmax, ymin, ymax, zmin, zmax)) # all integers nrs

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

Take the components from multiple inputs and merges them into one output. Except for the append axis, all inputs must have the same extent. All inputs must have the same number of scalar components. The output has the same origin and spacing as the first input. The origin and spacing of all other inputs are ignored. All inputs must have the same scalar type.

Arguments:
  • axis : (int, str) axis expanded to hold the multiple images
  • preserve_extents : (bool) if True, the extent of the inputs is used to place the image in the output. The whole extent of the output is the union of the input whole extents. Any portion of the output not covered by the inputs is set to zero. The origin and spacing is taken from the first input.
Example:
from vedo import Volume, dataurl
vol = Volume(dataurl+'embryo.tif')
vol.append(vol, axis='x').show().close()

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

Add the specified number of voxels at the Volume borders. Voxels can be a list formatted as [nx0, nx1, ny0, ny1, nz0, nz1].

Arguments:
  • voxels : (int, list) number of voxels to be added (or a list of length 4)
  • value : (int) intensity value (gray-scale color) of the padding
Example:
from vedo import Volume, dataurl, show
iso = Volume(dataurl+'embryo.tif').isosurface()
vol = iso.binarize(spacing=(100, 100, 100)).pad(10)
vol.dilate([15,15,15])
show(iso, vol.isosurface(), N=2, axes=1)

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

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

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

Normalize that scalar components for each point.

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

Mirror flip along one of the cartesian axes.

def operation(self, operation: str, volume2=None) -> Volume:
1192    def operation(self, operation: str, volume2=None) -> "Volume":
1193        """
1194        Perform operations with `Volume` objects.
1195        Keyword `volume2` can be a constant `float`.
1196
1197        Possible operations are:
1198        ```
1199        and, or, xor, nand, nor, not,
1200        +, -, /, 1/x, sin, cos, exp, log,
1201        abs, **2, sqrt, min, max, atan, atan2, median,
1202        mag, dot, gradient, divergence, laplacian.
1203        ```
1204
1205        Example:
1206        ```py
1207        from vedo import Box, show
1208        vol1 = Box(size=(35,10, 5)).binarize()
1209        vol2 = Box(size=( 5,10,35)).binarize()
1210        vol = vol1.operation("xor", vol2)
1211        show([[vol1, vol2], 
1212            ["vol1 xor vol2", vol]],
1213            N=2, axes=1, viewup="z",
1214        ).close()
1215        ```
1216
1217        Note:
1218            For logic operations, the two volumes must have the same bounds.
1219            If they do not, a larger image is created to contain both and the
1220            volumes are resampled onto the larger image before the operation is
1221            performed. This can be slow and memory intensive.
1222
1223        See also:
1224            - [volume_operations.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/volume_operations.py)
1225        """
1226        op = operation.lower()
1227        image1 = self.dataset
1228
1229        if op in ["and", "or", "xor", "nand", "nor"]:
1230
1231            if not np.allclose(image1.GetBounds(), volume2.dataset.GetBounds()):
1232                # create a larger image to contain both
1233                b1 = image1.GetBounds()
1234                b2 = volume2.dataset.GetBounds()
1235                b = [
1236                    min(b1[0], b2[0]),
1237                    max(b1[1], b2[1]),
1238                    min(b1[2], b2[2]),
1239                    max(b1[3], b2[3]),
1240                    min(b1[4], b2[4]),
1241                    max(b1[5], b2[5]),
1242                ]
1243                dims1 = image1.GetDimensions()
1244                dims2 = volume2.dataset.GetDimensions()
1245                dims = [max(dims1[0], dims2[0]), max(dims1[1], dims2[1]), max(dims1[2], dims2[2])]
1246
1247                image = vtki.vtkImageData()
1248                image.SetDimensions(dims)
1249                spacing = (
1250                    (b[1] - b[0]) / dims[0],
1251                    (b[3] - b[2]) / dims[1],
1252                    (b[5] - b[4]) / dims[2],
1253                )
1254                image.SetSpacing(spacing)
1255                image.SetOrigin((b[0], b[2], b[4]))
1256                image.AllocateScalars(vtki.VTK_UNSIGNED_CHAR, 1)
1257                image.GetPointData().GetScalars().FillComponent(0, 0)
1258
1259                interp1 = vtki.new("ImageReslice")
1260                interp1.SetInputData(image1)
1261                interp1.SetOutputExtent(image.GetExtent())
1262                interp1.SetOutputOrigin(image.GetOrigin())
1263                interp1.SetOutputSpacing(image.GetSpacing())
1264                interp1.SetInterpolationModeToNearestNeighbor()
1265                interp1.Update()
1266                imageA = interp1.GetOutput()
1267
1268                interp2 = vtki.new("ImageReslice")
1269                interp2.SetInputData(volume2.dataset)
1270                interp2.SetOutputExtent(image.GetExtent())
1271                interp2.SetOutputOrigin(image.GetOrigin())
1272                interp2.SetOutputSpacing(image.GetSpacing())
1273                interp2.SetInterpolationModeToNearestNeighbor()
1274                interp2.Update()
1275                imageB = interp2.GetOutput()
1276
1277            else:
1278                imageA = image1
1279                imageB = volume2.dataset
1280
1281            img_logic = vtki.new("ImageLogic")
1282            img_logic.SetInput1Data(imageA)
1283            img_logic.SetInput2Data(imageB)
1284            img_logic.SetOperation(["and", "or", "xor", "nand", "nor"].index(op))
1285            img_logic.Update()
1286
1287            out_vol = Volume(img_logic.GetOutput())
1288            out_vol.pipeline = utils.OperationNode(
1289                "operation", comment=f"{op}", parents=[self, volume2], c="#4cc9f0", shape="cylinder"
1290            )
1291            return out_vol  ######################################################
1292
1293        if volume2 and isinstance(volume2, Volume):
1294            # assert image1.GetScalarType() == volume2.dataset.GetScalarType(), "volumes have different scalar types"
1295            # make sure they have the same bounds:
1296            assert np.allclose(image1.GetBounds(), volume2.dataset.GetBounds()), "volumes have different bounds"
1297            # make sure they have the same spacing:
1298            assert np.allclose(image1.GetSpacing(), volume2.dataset.GetSpacing()), "volumes have different spacing"
1299            # make sure they have the same origin:
1300            assert np.allclose(image1.GetOrigin(), volume2.dataset.GetOrigin()), "volumes have different origin"
1301
1302        mf = None
1303        if op in ["median"]:
1304            mf = vtki.new("ImageMedian3D")
1305            mf.SetInputData(image1)
1306        elif op in ["mag"]:
1307            mf = vtki.new("ImageMagnitude")
1308            mf.SetInputData(image1)
1309        elif op in ["dot"]:
1310            mf = vtki.new("ImageDotProduct")
1311            mf.SetInput1Data(image1)
1312            mf.SetInput2Data(volume2.dataset)
1313        elif op in ["grad", "gradient"]:
1314            mf = vtki.new("ImageGradient")
1315            mf.SetDimensionality(3)
1316            mf.SetInputData(image1)
1317        elif op in ["div", "divergence"]:
1318            mf = vtki.new("ImageDivergence")
1319            mf.SetInputData(image1)
1320        elif op in ["laplacian"]:
1321            mf = vtki.new("ImageLaplacian")
1322            mf.SetDimensionality(3)
1323            mf.SetInputData(image1)
1324        elif op in ["not"]:
1325            mf = vtki.new("ImageLogic")
1326            mf.SetInput1Data(image1)
1327            mf.SetOperation(4)
1328
1329        if mf is not None:
1330            mf.Update()
1331            vol = Volume(mf.GetOutput())
1332            vol.pipeline = utils.OperationNode(
1333                "operation", comment=f"{op}", parents=[self], c="#4cc9f0", shape="cylinder"
1334            )
1335            return vol  ######################################################
1336
1337        mat = vtki.new("ImageMathematics")
1338        mat.SetInput1Data(image1)
1339
1340        K = None
1341
1342        if utils.is_number(volume2):
1343            K = volume2
1344            mat.SetConstantK(K)
1345            mat.SetConstantC(K)
1346
1347        elif volume2 is not None:  # assume image2 is a constant value
1348            mat.SetInput2Data(volume2.dataset)
1349
1350        # ###########################
1351        if op in ["+", "add", "plus"]:
1352            if K:
1353                mat.SetOperationToAddConstant()
1354            else:
1355                mat.SetOperationToAdd()
1356
1357        elif op in ["-", "subtract", "minus"]:
1358            if K:
1359                mat.SetConstantC(-float(K))
1360                mat.SetOperationToAddConstant()
1361            else:
1362                mat.SetOperationToSubtract()
1363
1364        elif op in ["*", "multiply", "times"]:
1365            if K:
1366                mat.SetOperationToMultiplyByK()
1367            else:
1368                mat.SetOperationToMultiply()
1369
1370        elif op in ["/", "divide"]:
1371            if K:
1372                mat.SetConstantK(1.0 / K)
1373                mat.SetOperationToMultiplyByK()
1374            else:
1375                mat.SetOperationToDivide()
1376
1377        elif op in ["1/x", "invert"]:
1378            mat.SetOperationToInvert()
1379        elif op in ["sin"]:
1380            mat.SetOperationToSin()
1381        elif op in ["cos"]:
1382            mat.SetOperationToCos()
1383        elif op in ["exp"]:
1384            mat.SetOperationToExp()
1385        elif op in ["log"]:
1386            mat.SetOperationToLog()
1387        elif op in ["abs"]:
1388            mat.SetOperationToAbsoluteValue()
1389        elif op in ["**2", "square"]:
1390            mat.SetOperationToSquare()
1391        elif op in ["sqrt", "sqr"]:
1392            mat.SetOperationToSquareRoot()
1393        elif op in ["min"]:
1394            mat.SetOperationToMin()
1395        elif op in ["max"]:
1396            mat.SetOperationToMax()
1397        elif op in ["atan"]:
1398            mat.SetOperationToATAN()
1399        elif op in ["atan2"]:
1400            mat.SetOperationToATAN2()
1401        else:
1402            vedo.logger.error(f"unknown operation {operation}")
1403            raise RuntimeError()
1404        mat.Update()
1405
1406        self._update(mat.GetOutput())
1407
1408        self.pipeline = utils.OperationNode(
1409            "operation", comment=f"{op}", parents=[self, volume2], shape="cylinder", c="#4cc9f0"
1410        )
1411        return self

Perform operations with Volume objects. Keyword volume2 can be a constant float.

Possible operations are:

and, or, xor, nand, nor, not,
+, -, /, 1/x, sin, cos, exp, log,
abs, **2, sqrt, min, max, atan, atan2, median,
mag, dot, gradient, divergence, laplacian.

Example:

from vedo import Box, show
vol1 = Box(size=(35,10, 5)).binarize()
vol2 = Box(size=( 5,10,35)).binarize()
vol = vol1.operation("xor", vol2)
show([[vol1, vol2], 
    ["vol1 xor vol2", vol]],
    N=2, axes=1, viewup="z",
).close()
Note:

For logic operations, the two volumes must have the same bounds. If they do not, a larger image is created to contain both and the volumes are resampled onto the larger image before the operation is performed. This can be slow and memory intensive.

See also:
def frequency_pass_filter(self, low_cutoff=None, high_cutoff=None, order=1) -> Self:
1413    def frequency_pass_filter(self, low_cutoff=None, high_cutoff=None, order=1) -> Self:
1414        """
1415        Low-pass and high-pass filtering become trivial in the frequency domain.
1416        A portion of the pixels/voxels are simply masked or attenuated.
1417        This function applies a high pass Butterworth filter that attenuates the
1418        frequency domain image.
1419
1420        The gradual attenuation of the filter is important.
1421        A simple high-pass filter would simply mask a set of pixels in the frequency domain,
1422        but the abrupt transition would cause a ringing effect in the spatial domain.
1423
1424        Arguments:
1425            low_cutoff : (list)
1426                the cutoff frequencies for x, y and z
1427            high_cutoff : (list)
1428                the cutoff frequencies for x, y and z
1429            order : (int)
1430                order determines sharpness of the cutoff curve
1431        """
1432        # https://lorensen.github.io/VTKExamples/site/Cxx/ImageProcessing/IdealHighPass
1433        fft = vtki.new("ImageFFT")
1434        fft.SetInputData(self.dataset)
1435        fft.Update()
1436        out = fft.GetOutput()
1437
1438        if high_cutoff:
1439            blp = vtki.new("ImageButterworthLowPass")
1440            blp.SetInputData(out)
1441            blp.SetCutOff(high_cutoff)
1442            blp.SetOrder(order)
1443            blp.Update()
1444            out = blp.GetOutput()
1445
1446        if low_cutoff:
1447            bhp = vtki.new("ImageButterworthHighPass")
1448            bhp.SetInputData(out)
1449            bhp.SetCutOff(low_cutoff)
1450            bhp.SetOrder(order)
1451            bhp.Update()
1452            out = bhp.GetOutput()
1453
1454        rfft = vtki.new("ImageRFFT")
1455        rfft.SetInputData(out)
1456        rfft.Update()
1457
1458        ecomp = vtki.new("ImageExtractComponents")
1459        ecomp.SetInputData(rfft.GetOutput())
1460        ecomp.SetComponents(0)
1461        ecomp.Update()
1462        self._update(ecomp.GetOutput())
1463        self.pipeline = utils.OperationNode("frequency_pass_filter", parents=[self], c="#4cc9f0")
1464        return self

Low-pass and high-pass filtering become trivial in the frequency domain. A portion of the pixels/voxels are simply masked or attenuated. This function applies a high pass Butterworth filter that attenuates the frequency domain image.

The gradual attenuation of the filter is important. A simple high-pass filter would simply mask a set of pixels in the frequency domain, but the abrupt transition would cause a ringing effect in the spatial domain.

Arguments:
  • low_cutoff : (list) the cutoff frequencies for x, y and z
  • high_cutoff : (list) the cutoff frequencies for x, y and z
  • order : (int) order determines sharpness of the cutoff curve
def smooth_gaussian(self, sigma=(2, 2, 2), radius=None) -> Self:
1466    def smooth_gaussian(self, sigma=(2, 2, 2), radius=None) -> Self:
1467        """
1468        Performs a convolution of the input Volume with a gaussian.
1469
1470        Arguments:
1471            sigma : (float, list)
1472                standard deviation(s) in voxel units.
1473                A list can be given to smooth in the three direction differently.
1474            radius : (float, list)
1475                radius factor(s) determine how far out the gaussian
1476                kernel will go before being clamped to zero. A list can be given too.
1477        """
1478        gsf = vtki.new("ImageGaussianSmooth")
1479        gsf.SetDimensionality(3)
1480        gsf.SetInputData(self.dataset)
1481        if utils.is_sequence(sigma):
1482            gsf.SetStandardDeviations(sigma)
1483        else:
1484            gsf.SetStandardDeviation(sigma)
1485        if radius is not None:
1486            if utils.is_sequence(radius):
1487                gsf.SetRadiusFactors(radius)
1488            else:
1489                gsf.SetRadiusFactor(radius)
1490        gsf.Update()
1491        self._update(gsf.GetOutput())
1492        self.pipeline = utils.OperationNode("smooth_gaussian", parents=[self], c="#4cc9f0")
1493        return self

Performs a convolution of the input Volume with a gaussian.

Arguments:
  • sigma : (float, list) standard deviation(s) in voxel units. A list can be given to smooth in the three direction differently.
  • radius : (float, list) radius factor(s) determine how far out the gaussian kernel will go before being clamped to zero. A list can be given too.
def smooth_median(self, neighbours=(2, 2, 2)) -> Self:
1495    def smooth_median(self, neighbours=(2, 2, 2)) -> Self:
1496        """
1497        Median filter that replaces each pixel with the median value
1498        from a rectangular neighborhood around that pixel.
1499        """
1500        imgm = vtki.new("ImageMedian3D")
1501        imgm.SetInputData(self.dataset)
1502        if utils.is_sequence(neighbours):
1503            imgm.SetKernelSize(neighbours[0], neighbours[1], neighbours[2])
1504        else:
1505            imgm.SetKernelSize(neighbours, neighbours, neighbours)
1506        imgm.Update()
1507        self._update(imgm.GetOutput())
1508        self.pipeline = utils.OperationNode("smooth_median", parents=[self], c="#4cc9f0")
1509        return self

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

def erode(self, neighbours=(2, 2, 2)) -> Self:
1511    def erode(self, neighbours=(2, 2, 2)) -> Self:
1512        """
1513        Replace a voxel with the minimum over an ellipsoidal neighborhood of voxels.
1514        If `neighbours` of an axis is 1, no processing is done on that axis.
1515
1516        Examples:
1517            - [erode_dilate.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/erode_dilate.py)
1518
1519                ![](https://vedo.embl.es/images/volumetric/erode_dilate.png)
1520        """
1521        ver = vtki.new("ImageContinuousErode3D")
1522        ver.SetInputData(self.dataset)
1523        ver.SetKernelSize(neighbours[0], neighbours[1], neighbours[2])
1524        ver.Update()
1525        self._update(ver.GetOutput())
1526        self.pipeline = utils.OperationNode("erode", parents=[self], c="#4cc9f0")
1527        return self

Replace a voxel with the minimum over an ellipsoidal neighborhood of voxels. If neighbours of an axis is 1, no processing is done on that axis.

Examples:
def dilate(self, neighbours=(2, 2, 2)) -> Self:
1529    def dilate(self, neighbours=(2, 2, 2)) -> Self:
1530        """
1531        Replace a voxel with the maximum over an ellipsoidal neighborhood of voxels.
1532        If `neighbours` of an axis is 1, no processing is done on that axis.
1533
1534        Check also `erode()` and `pad()`.
1535
1536        Examples:
1537            - [erode_dilate.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/erode_dilate.py)
1538        """
1539        ver = vtki.new("ImageContinuousDilate3D")
1540        ver.SetInputData(self.dataset)
1541        ver.SetKernelSize(neighbours[0], neighbours[1], neighbours[2])
1542        ver.Update()
1543        self._update(ver.GetOutput())
1544        self.pipeline = utils.OperationNode("dilate", parents=[self], c="#4cc9f0")
1545        return self

Replace a voxel with the maximum over an ellipsoidal neighborhood of voxels. If neighbours of an axis is 1, no processing is done on that axis.

Check also erode() and pad().

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

Colapses components with magnitude function.

def topoints(self) -> vedo.pointcloud.Points:
1556    def topoints(self) -> "vedo.Points":
1557        """
1558        Extract all image voxels as points.
1559        This function takes an input `Volume` and creates an `Mesh`
1560        that contains the points and the point attributes.
1561
1562        Examples:
1563            - [vol2points.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/vol2points.py)
1564        """
1565        v2p = vtki.new("ImageToPoints")
1566        v2p.SetInputData(self.dataset)
1567        v2p.Update()
1568        mpts = vedo.Points(v2p.GetOutput())
1569        mpts.pipeline = utils.OperationNode("topoints", parents=[self], c="#4cc9f0:#e9c46a")
1570        return mpts

Extract all image voxels as points. This function takes an input Volume and creates an Mesh that contains the points and the point attributes.

Examples:
def euclidean_distance(self, anisotropy=False, max_distance=None) -> Volume:
1572    def euclidean_distance(self, anisotropy=False, max_distance=None) -> "Volume":
1573        """
1574        Implementation of the Euclidean DT (Distance Transform) using Saito's algorithm.
1575        The distance map produced contains the square of the Euclidean distance values.
1576        The algorithm has a O(n^(D+1)) complexity over n x n x...x n images in D dimensions.
1577
1578        Check out also: https://en.wikipedia.org/wiki/Distance_transform
1579
1580        Arguments:
1581            anisotropy : bool
1582                used to define whether Spacing should be used in the
1583                computation of the distances.
1584            max_distance : bool
1585                any distance bigger than max_distance will not be
1586                computed but set to this specified value instead.
1587
1588        Examples:
1589            - [euclidian_dist.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/euclidian_dist.py)
1590        """
1591        euv = vtki.new("ImageEuclideanDistance")
1592        euv.SetInputData(self.dataset)
1593        euv.SetConsiderAnisotropy(anisotropy)
1594        if max_distance is not None:
1595            euv.InitializeOn()
1596            euv.SetMaximumDistance(max_distance)
1597        euv.SetAlgorithmToSaito()
1598        euv.Update()
1599        vol = Volume(euv.GetOutput())
1600        vol.pipeline = utils.OperationNode("euclidean_distance", parents=[self], c="#4cc9f0")
1601        return vol

Implementation of the Euclidean DT (Distance Transform) using Saito's algorithm. The distance map produced contains the square of the Euclidean distance values. The algorithm has a O(n^(D+1)) complexity over n x n x...x n images in D dimensions.

Check out also: https://en.wikipedia.org/wiki/Distance_transform

Arguments:
  • anisotropy : bool used to define whether Spacing should be used in the computation of the distances.
  • max_distance : bool any distance bigger than max_distance will not be computed but set to this specified value instead.
Examples:
def correlation_with(self, vol2: Volume, dim=2) -> Volume:
1603    def correlation_with(self, vol2: "Volume", dim=2) -> "Volume":
1604        """
1605        Find the correlation between two volumetric data sets.
1606        Keyword `dim` determines whether the correlation will be 3D, 2D or 1D.
1607        The default is a 2D Correlation.
1608
1609        The output size will match the size of the first input.
1610        The second input is considered the correlation kernel.
1611        """
1612        imc = vtki.new("ImageCorrelation")
1613        imc.SetInput1Data(self.dataset)
1614        imc.SetInput2Data(vol2.dataset)
1615        imc.SetDimensionality(dim)
1616        imc.Update()
1617        vol = Volume(imc.GetOutput())
1618
1619        vol.pipeline = utils.OperationNode("correlation_with", parents=[self, vol2], c="#4cc9f0")
1620        return vol

Find the correlation between two volumetric data sets. Keyword dim determines whether the correlation will be 3D, 2D or 1D. The default is a 2D Correlation.

The output size will match the size of the first input. The second input is considered the correlation kernel.

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

Scale the voxel content by factor scale.