vedo.volume

Work with volumetric datasets (voxel data).

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

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
211    @property
212    def mapper(self):
213        """Return the underlying `vtkMapper` object."""
214        return self.actor.GetMapper()

Return the underlying vtkMapper object.

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

Deprecated. Use Volume.cmap() instead.

def copy(self, deep=True) -> Volume:
387    def copy(self, deep=True) -> "Volume":
388        """Return a copy of the Volume. Alias of `clone()`."""
389        return self.clone(deep=deep)

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

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

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

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

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

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

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

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

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

def zslice(self, k: int) -> vedo.mesh.Mesh:
462    def zslice(self, k: int) -> Mesh:
463        """Extract the slice at index `i` of volume along z-axis."""
464        vslice = vtki.new("ImageDataGeometryFilter")
465        vslice.SetInputData(self.dataset)
466        nx, ny, nz = self.dataset.GetDimensions()
467        if k > nz - 1:
468            k = nz - 1
469        vslice.SetExtent(0, nx, 0, ny, k, k)
470        vslice.Update()
471        m = Mesh(vslice.GetOutput())
472        m.pipeline = utils.OperationNode(f"zslice {k}", parents=[self], c="#4cc9f0:#e9c46a")
473        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:
475    def slice_plane(self, origin: List[float], normal: List[float], autocrop=False, border=0.5, mode="linear") -> Mesh:
476        """
477        Extract the slice along a given plane position and normal.
478
479        Two metadata arrays are added to the output Mesh:
480            - "shape" : contains the shape of the slice
481            - "original_bounds" : contains the original bounds of the slice
482        One can access them with e.g. `myslice.metadata["shape"]`.
483
484        Arguments:
485            origin : (list)
486                position of the plane
487            normal : (list)
488                normal to the plane
489            autocrop : (bool)
490                crop the output to the minimal possible size
491            border : (float)
492                add a border to the output slice
493            mode : (str)
494                interpolation mode, one of the following: "linear", "nearest", "cubic"
495
496        Example:
497            - [slice_plane1.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slice_plane1.py)
498
499                ![](https://vedo.embl.es/images/volumetric/slicePlane1.gif)
500
501            - [slice_plane2.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slice_plane2.py)
502
503                ![](https://vedo.embl.es/images/volumetric/slicePlane2.png)
504
505            - [slice_plane3.py](https://github.com/marcomusy/vedo/tree/master/examples/volumetric/slice_plane3.py)
506
507                ![](https://vedo.embl.es/images/volumetric/slicePlane3.jpg)
508        """
509        newaxis = utils.versor(normal)
510        pos = np.array(origin)
511        initaxis = (0, 0, 1)
512        crossvec = np.cross(initaxis, newaxis)
513        angle = np.arccos(np.dot(initaxis, newaxis))
514        T = vtki.vtkTransform()
515        T.PostMultiply()
516        T.RotateWXYZ(np.rad2deg(angle), crossvec.tolist())
517        T.Translate(pos.tolist())
518
519        reslice = vtki.new("ImageReslice")
520        reslice.SetResliceAxes(T.GetMatrix())
521        reslice.SetInputData(self.dataset)
522        reslice.SetOutputDimensionality(2)
523        reslice.SetTransformInputSampling(True)
524        reslice.SetGenerateStencilOutput(False)
525        if border:
526            reslice.SetBorder(True)
527            reslice.SetBorderThickness(border)
528        else:
529            reslice.SetBorder(False)
530        if mode == "linear":
531            reslice.SetInterpolationModeToLinear()
532        elif mode == "nearest":
533            reslice.SetInterpolationModeToNearestNeighbor()
534        elif mode == "cubic":
535            reslice.SetInterpolationModeToCubic()
536        else:
537            vedo.logger.error(f"in slice_plane(): unknown interpolation mode {mode}")
538            raise ValueError()
539        reslice.SetAutoCropOutput(not autocrop)
540        reslice.Update()
541        img = reslice.GetOutput()
542
543        vslice = vtki.new("ImageDataGeometryFilter")
544        vslice.SetInputData(img)
545        vslice.Update()
546
547        msh = Mesh(vslice.GetOutput()).apply_transform(T)
548        msh.properties.LightingOff()
549
550        d0, d1, _ = img.GetDimensions()
551        varr1 = utils.numpy2vtk([d1, d0], name="shape")
552        varr2 = utils.numpy2vtk(img.GetBounds(), name="original_bounds")
553        msh.dataset.GetFieldData().AddArray(varr1)
554        msh.dataset.GetFieldData().AddArray(varr2)
555        msh.pipeline = utils.OperationNode("slice_plane", parents=[self], c="#4cc9f0:#e9c46a")
556        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:
558    def slab(self, slice_range=(), axis='z', operation="mean") -> Mesh:
559        """
560        Extract a slab from a `Volume` by combining
561        all of the slices of an image to create a single slice.
562
563        Returns a `Mesh` containing metadata which
564        can be accessed with e.g. `mesh.metadata["slab_range"]`.
565
566        Metadata:
567            slab_range : (list)
568                contains the range of slices extracted
569            slab_axis : (str)
570                contains the axis along which the slab was extracted
571            slab_operation : (str)
572                contains the operation performed on the slab
573            slab_bounding_box : (list)
574                contains the bounding box of the slab
575
576        Arguments:
577            slice_range : (list)
578                range of slices to extract
579            axis : (str)
580                axis along which to extract the slab
581            operation : (str)
582                operation to perform on the slab,
583                allowed values are: "sum", "min", "max", "mean".
584
585        Example:
586            - [slab.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/slab_vol.py)
587
588            ![](https://vedo.embl.es/images/volumetric/slab_vol.jpg)
589        """
590        if len(slice_range) != 2:
591            vedo.logger.error("in slab(): slice_range is empty or invalid")
592            raise ValueError()
593
594        islab = vtki.new("ImageSlab")
595        islab.SetInputData(self.dataset)
596
597        if operation in ["+", "add", "sum"]:
598            islab.SetOperationToSum()
599        elif "min" in operation:
600            islab.SetOperationToMin()
601        elif "max" in operation:
602            islab.SetOperationToMax()
603        elif "mean" in operation:
604            islab.SetOperationToMean()
605        else:
606            vedo.logger.error(f"in slab(): unknown operation {operation}")
607            raise ValueError()
608
609        dims = self.dimensions()
610        if axis == 'x':
611            islab.SetOrientationToX()
612            if slice_range[0]  > dims[0]-1:
613                slice_range[0] = dims[0]-1
614            if slice_range[1]  > dims[0]-1:
615                slice_range[1] = dims[0]-1
616        elif axis == 'y':
617            islab.SetOrientationToY()
618            if slice_range[0]  > dims[1]-1:
619                slice_range[0] = dims[1]-1
620            if slice_range[1]  > dims[1]-1:
621                slice_range[1] = dims[1]-1
622        elif axis == 'z':
623            islab.SetOrientationToZ()
624            if slice_range[0]  > dims[2]-1:
625                slice_range[0] = dims[2]-1
626            if slice_range[1]  > dims[2]-1:
627                slice_range[1] = dims[2]-1
628        else:
629            vedo.logger.error(f"Error in slab(): unknown axis {axis}")
630            raise RuntimeError()
631
632        islab.SetSliceRange(slice_range)
633        islab.Update()
634
635        msh = Mesh(islab.GetOutput()).lighting('off')
636        msh.mapper.SetLookupTable(utils.ctf2lut(self, msh))
637        msh.mapper.SetScalarRange(self.scalar_range())
638
639        msh.metadata["slab_range"] = slice_range
640        msh.metadata["slab_axis"]  = axis
641        msh.metadata["slab_operation"] = operation
642
643        # compute bounds of slab
644        origin = list(self.origin())
645        spacing = list(self.spacing())
646        if axis == 'x':
647            msh.metadata["slab_bounding_box"] = [
648                origin[0] + slice_range[0]*spacing[0],
649                origin[0] + slice_range[1]*spacing[0],
650                origin[1],
651                origin[1] + dims[1]*spacing[1],
652                origin[2],
653                origin[2] + dims[2]*spacing[2],
654            ]
655        elif axis == 'y':
656            msh.metadata["slab_bounding_box"] = [
657                origin[0],
658                origin[0] + dims[0]*spacing[0],
659                origin[1] + slice_range[0]*spacing[1],
660                origin[1] + slice_range[1]*spacing[1],
661                origin[2],
662                origin[2] + dims[2]*spacing[2],
663            ]
664        elif axis == 'z':
665            msh.metadata["slab_bounding_box"] = [
666                origin[0],
667                origin[0] + dims[0]*spacing[0],
668                origin[1],
669                origin[1] + dims[1]*spacing[1],
670                origin[2] + slice_range[0]*spacing[2],
671                origin[2] + slice_range[1]*spacing[2],
672            ]
673
674        msh.pipeline = utils.OperationNode(
675            f"slab{slice_range}",
676            comment=f"axis={axis}, operation={operation}",
677            parents=[self],
678            c="#4cc9f0:#e9c46a",
679        )
680        msh.name = "SlabMesh"
681        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:
684    def warp(
685            self,
686            source: Union["vedo.Points", List],
687            target: Union["vedo.Points", List],
688            sigma=1, mode="3d", fit=True,
689        ) -> Self:
690        """
691        Warp volume scalars within a Volume by specifying
692        source and target sets of points.
693
694        Arguments:
695            source : (Points, list)
696                the list of source points
697            target : (Points, list)
698                the list of target points
699            fit : (bool)
700                fit/adapt the old bounding box to the warped geometry
701        """
702        if isinstance(source, vedo.Points):
703            source = source.coordinates
704        if isinstance(target, vedo.Points):
705            target = target.coordinates
706
707        NLT = transformations.NonLinearTransform()
708        NLT.source_points = source
709        NLT.target_points = target
710        NLT.sigma = sigma
711        NLT.mode = mode
712
713        self.apply_transform(NLT, fit=fit)
714        self.pipeline = utils.OperationNode("warp", parents=[self], c="#4cc9f0")
715        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:
717    def apply_transform(
718            self,
719            T: Union[transformations.LinearTransform, transformations.NonLinearTransform],
720            fit=True, interpolation="cubic",
721        ) -> Self:
722        """
723        Apply a transform to the scalars in the volume.
724
725        Arguments:
726            T : (LinearTransform, NonLinearTransform)
727                The transformation to be applied
728            fit : (bool)
729                fit/adapt the old bounding box to the modified geometry
730            interpolation : (str)
731                one of the following: "nearest", "linear", "cubic"
732        """
733        if utils.is_sequence(T):
734            T = transformations.LinearTransform(T)
735
736        TI = T.compute_inverse()
737
738        reslice = vtki.new("ImageReslice")
739        reslice.SetInputData(self.dataset)
740        reslice.SetResliceTransform(TI.T)
741        reslice.SetOutputDimensionality(3)
742        if "lin" in interpolation.lower():
743            reslice.SetInterpolationModeToLinear()
744        elif "near" in interpolation.lower():
745            reslice.SetInterpolationModeToNearestNeighbor()
746        elif "cubic" in interpolation.lower():
747            reslice.SetInterpolationModeToCubic()
748        else:
749            vedo.logger.error(
750                f"in apply_transform: unknown interpolation mode {interpolation}")
751            raise ValueError()
752        reslice.SetAutoCropOutput(fit)
753        reslice.Update()
754        self._update(reslice.GetOutput())
755        self.transform = T
756        self.pipeline = utils.OperationNode(
757            "apply_transform", parents=[self], c="#4cc9f0")
758        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:
760    def imagedata(self) -> vtki.vtkImageData:
761        """
762        DEPRECATED:
763        Use `Volume.dataset` instead.
764
765        Return the underlying `vtkImagaData` object.
766        """
767        print("Volume.imagedata() is deprecated, use Volume.dataset instead")
768        return self.dataset

DEPRECATED: Use Volume.dataset instead.

Return the underlying vtkImagaData object.

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

Mark the object as modified.

Example:

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

Return the nr. of voxels in the 3 dimensions.

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

Return the nr. of voxels in the 3 dimensions.

def scalar_range(self) -> numpy.ndarray:
821    def scalar_range(self) -> np.ndarray:
822        """Return the range of the scalar values."""
823        return np.array(self.dataset.GetScalarRange())

Return the range of the scalar values.

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

Set/get the voxels size in the 3 dimensions.

def origin(self, s=None) -> Union[Self, Iterable[float]]:
832    def origin(self, s=None) -> Union[Self, Iterable[float]]:
833        """
834        Set/get the origin of the volumetric dataset.
835
836        The origin is the position in world coordinates of the point index (0,0,0).
837        This point does not have to be part of the dataset, in other words,
838        the dataset extent does not have to start at (0,0,0) and the origin
839        can be outside of the dataset bounding box.
840        The origin plus spacing determine the position in space of the points.
841        """
842        if s is not None:
843            self.dataset.SetOrigin(s)
844            return self
845        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]]:
847    def pos(self, p=None) -> Union[Self, Iterable[float]]:
848        """Set/get the position of the volumetric dataset."""
849        if p is not None:
850            self.origin(p)
851            return self
852        return self.origin()

Set/get the position of the volumetric dataset.

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

Get the center of the volumetric dataset.

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

Shift the volumetric dataset by a vector.

def rotate_x(self, angle: float, rad=False, around=None) -> Self:
864    def rotate_x(self, angle: float, rad=False, around=None) -> Self:
865        """
866        Rotate around x-axis. If angle is in radians set `rad=True`.
867
868        Use `around` to define a pivoting point.
869        """
870        if angle == 0:
871            return self
872        LT = transformations.LinearTransform().rotate_x(angle, rad, around)
873        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:
875    def rotate_y(self, angle: float, rad=False, around=None) -> Self:
876        """
877        Rotate around y-axis. If angle is in radians set `rad=True`.
878
879        Use `around` to define a pivoting point.
880        """
881        if angle == 0:
882            return self
883        LT = transformations.LinearTransform().rotate_y(angle, rad, around)
884        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:
886    def rotate_z(self, angle: float, rad=False, around=None) -> Self:
887        """
888        Rotate around z-axis. If angle is in radians set `rad=True`.
889
890        Use `around` to define a pivoting point.
891        """
892        if angle == 0:
893            return self
894        LT = transformations.LinearTransform().rotate_z(angle, rad, around)
895        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:
897    def get_cell_from_ijk(self, ijk: list) -> int:
898        """
899        Get the voxel id number at the given ijk coordinates.
900
901        Arguments:
902            ijk : (list)
903                the ijk coordinates of the voxel
904        """
905        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:
907    def get_point_from_ijk(self, ijk: list) -> int:
908        """
909        Get the point id number at the given ijk coordinates.
910
911        Arguments:
912            ijk : (list)
913                the ijk coordinates of the voxel
914        """
915        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:
917    def permute_axes(self, x: int, y: int, z: int) -> Self:
918        """
919        Reorder the axes of the Volume by specifying
920        the input axes which are supposed to become the new X, Y, and Z.
921        """
922        imp = vtki.new("ImagePermute")
923        imp.SetFilteredAxes(x, y, z)
924        imp.SetInputData(self.dataset)
925        imp.Update()
926        self._update(imp.GetOutput())
927        self.pipeline = utils.OperationNode(
928            f"permute_axes({(x,y,z)})", parents=[self], c="#4cc9f0"
929        )
930        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:
932    def resample(self, new_spacing: List[float], interpolation=1) -> Self:
933        """
934        Resamples a `Volume` to be larger or smaller.
935
936        This method modifies the spacing of the input.
937        Linear interpolation is used to resample the data.
938
939        Arguments:
940            new_spacing : (list)
941                a list of 3 new spacings for the 3 axes
942            interpolation : (int)
943                0=nearest_neighbor, 1=linear, 2=cubic
944        """
945        rsp = vtki.new("ImageResample")
946        oldsp = self.spacing()
947        for i in range(3):
948            if oldsp[i] != new_spacing[i]:
949                rsp.SetAxisOutputSpacing(i, new_spacing[i])
950        rsp.InterpolateOn()
951        rsp.SetInterpolationMode(interpolation)
952        rsp.OptimizationOn()
953        rsp.Update()
954        self._update(rsp.GetOutput())
955        self.pipeline = utils.OperationNode(
956            "resample", comment=f"spacing: {tuple(new_spacing)}", parents=[self], c="#4cc9f0"
957        )
958        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:
 961    def threshold(self, above=None, below=None, replace=None, replace_value=None) -> Self:
 962        """
 963        Binary or continuous volume thresholding.
 964        Find the voxels that contain a value above/below the input values
 965        and replace them with a new value (default is 0).
 966        """
 967        th = vtki.new("ImageThreshold")
 968        th.SetInputData(self.dataset)
 969
 970        # sanity checks
 971        if above is not None and below is not None:
 972            if above == below:
 973                return self
 974            if above > below:
 975                vedo.logger.warning("in volume.threshold(), above > below, skip.")
 976                return self
 977
 978        ## cases
 979        if below is not None and above is not None:
 980            th.ThresholdBetween(above, below)
 981
 982        elif above is not None:
 983            th.ThresholdByUpper(above)
 984
 985        elif below is not None:
 986            th.ThresholdByLower(below)
 987
 988        ##
 989        if replace is not None:
 990            th.SetReplaceIn(True)
 991            th.SetInValue(replace)
 992        else:
 993            th.SetReplaceIn(False)
 994
 995        if replace_value is not None:
 996            th.SetReplaceOut(True)
 997            th.SetOutValue(replace_value)
 998        else:
 999            th.SetReplaceOut(False)
1000
1001        th.Update()
1002        self._update(th.GetOutput())
1003        self.pipeline = utils.OperationNode("threshold", parents=[self], c="#4cc9f0")
1004        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:
1006    def crop(self, left=None, right=None, back=None, front=None, bottom=None, top=None, VOI=()) -> Self:
1007        """
1008        Crop a `Volume` object.
1009
1010        Arguments:
1011            left : (float)
1012                fraction to crop from the left plane (negative x)
1013            right : (float)
1014                fraction to crop from the right plane (positive x)
1015            back : (float)
1016                fraction to crop from the back plane (negative y)
1017            front : (float)
1018                fraction to crop from the front plane (positive y)
1019            bottom : (float)
1020                fraction to crop from the bottom plane (negative z)
1021            top : (float)
1022                fraction to crop from the top plane (positive z)
1023            VOI : (list)
1024                extract Volume Of Interest expressed in voxel numbers
1025
1026        Example:
1027            `vol.crop(VOI=(xmin, xmax, ymin, ymax, zmin, zmax)) # all integers nrs`
1028        """
1029        extractVOI = vtki.new("ExtractVOI")
1030        extractVOI.SetInputData(self.dataset)
1031
1032        if VOI:
1033            extractVOI.SetVOI(VOI)
1034        else:
1035            d = self.dataset.GetDimensions()
1036            bx0, bx1, by0, by1, bz0, bz1 = 0, d[0]-1, 0, d[1]-1, 0, d[2]-1
1037            if left is not None:   bx0 = int((d[0]-1)*left)
1038            if right is not None:  bx1 = int((d[0]-1)*(1-right))
1039            if back is not None:   by0 = int((d[1]-1)*back)
1040            if front is not None:  by1 = int((d[1]-1)*(1-front))
1041            if bottom is not None: bz0 = int((d[2]-1)*bottom)
1042            if top is not None:    bz1 = int((d[2]-1)*(1-top))
1043            extractVOI.SetVOI(bx0, bx1, by0, by1, bz0, bz1)
1044        extractVOI.Update()
1045        self._update(extractVOI.GetOutput())
1046
1047        self.pipeline = utils.OperationNode(
1048            "crop", parents=[self], c="#4cc9f0", comment=f"dims={tuple(self.dimensions())}"
1049        )
1050        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:
1052    def append(self, *volumes, axis="z", preserve_extents=False) -> Self:
1053        """
1054        Take the components from multiple inputs and merges them into one output.
1055        Except for the append axis, all inputs must have the same extent.
1056        All inputs must have the same number of scalar components.
1057        The output has the same origin and spacing as the first input.
1058        The origin and spacing of all other inputs are ignored.
1059        All inputs must have the same scalar type.
1060
1061        Arguments:
1062            axis : (int, str)
1063                axis expanded to hold the multiple images
1064            preserve_extents : (bool)
1065                if True, the extent of the inputs is used to place
1066                the image in the output. The whole extent of the output is the union of the input
1067                whole extents. Any portion of the output not covered by the inputs is set to zero.
1068                The origin and spacing is taken from the first input.
1069
1070        Example:
1071            ```python
1072            from vedo import Volume, dataurl
1073            vol = Volume(dataurl+'embryo.tif')
1074            vol.append(vol, axis='x').show().close()
1075            ```
1076            ![](https://vedo.embl.es/images/feats/volume_append.png)
1077        """
1078        ima = vtki.new("ImageAppend")
1079        ima.SetInputData(self.dataset)
1080        # if not utils.is_sequence(volumes):
1081        #     volumes = [volumes]
1082        for volume in volumes:
1083            if isinstance(volume, vtki.vtkImageData):
1084                ima.AddInputData(volume)
1085            else:
1086                ima.AddInputData(volume.dataset)
1087        ima.SetPreserveExtents(preserve_extents)
1088        if axis == "x":
1089            axis = 0
1090        elif axis == "y":
1091            axis = 1
1092        elif axis == "z":
1093            axis = 2
1094        ima.SetAppendAxis(axis)
1095        ima.Update()
1096        self._update(ima.GetOutput())
1097
1098        self.pipeline = utils.OperationNode(
1099            "append",
1100            parents=[self, *volumes],
1101            c="#4cc9f0",
1102            comment=f"dims={tuple(self.dimensions())}",
1103        )
1104        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:
1106    def pad(self, voxels=10, value=0) -> Self:
1107        """
1108        Add the specified number of voxels at the `Volume` borders.
1109        Voxels can be a list formatted as `[nx0, nx1, ny0, ny1, nz0, nz1]`.
1110
1111        Arguments:
1112            voxels : (int, list)
1113                number of voxels to be added (or a list of length 4)
1114            value : (int)
1115                intensity value (gray-scale color) of the padding
1116
1117        Example:
1118            ```python
1119            from vedo import Volume, dataurl, show
1120            iso = Volume(dataurl+'embryo.tif').isosurface()
1121            vol = iso.binarize(spacing=(100, 100, 100)).pad(10)
1122            vol.dilate([15,15,15])
1123            show(iso, vol.isosurface(), N=2, axes=1)
1124            ```
1125            ![](https://vedo.embl.es/images/volumetric/volume_pad.png)
1126        """
1127        x0, x1, y0, y1, z0, z1 = self.dataset.GetExtent()
1128        pf = vtki.new("ImageConstantPad")
1129        pf.SetInputData(self.dataset)
1130        pf.SetConstant(value)
1131        if utils.is_sequence(voxels):
1132            pf.SetOutputWholeExtent(
1133                x0 - voxels[0], x1 + voxels[1],
1134                y0 - voxels[2], y1 + voxels[3],
1135                z0 - voxels[4], z1 + voxels[5],
1136            )
1137        else:
1138            pf.SetOutputWholeExtent(
1139                x0 - voxels, x1 + voxels,
1140                y0 - voxels, y1 + voxels,
1141                z0 - voxels, z1 + voxels,
1142            )
1143        pf.Update()
1144        self._update(pf.GetOutput())
1145        self.pipeline = utils.OperationNode(
1146            "pad", comment=f"{voxels} voxels", parents=[self], c="#f28482"
1147        )
1148        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:
1150    def resize(self, newdims: List[int]) -> Self:
1151        """Increase or reduce the number of voxels of a Volume with interpolation."""
1152        rsz = vtki.new("ImageResize")
1153        rsz.SetResizeMethodToOutputDimensions()
1154        rsz.SetInputData(self.dataset)
1155        rsz.SetOutputDimensions(newdims)
1156        rsz.Update()
1157        self.dataset = rsz.GetOutput()
1158        self._update(self.dataset)
1159        self.pipeline = utils.OperationNode(
1160            "resize", parents=[self], c="#4cc9f0", comment=f"dims={tuple(self.dimensions())}"
1161        )
1162        return self

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

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

Normalize that scalar components for each point.

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

Mirror flip along one of the cartesian axes.

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

Colapses components with magnitude function.

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

Scale the voxel content by factor scale.