vedo.image

Submodule to work with common format images.

   1#!/usr/bin/env python3
   2# -*- coding: utf-8 -*-
   3import numpy as np
   4from weakref import ref as weak_ref_to
   5from typing import Self, Tuple, List, Union, Any
   6
   7import vedo.vtkclasses as vtki
   8
   9import vedo
  10from vedo import colors
  11from vedo import utils
  12
  13__docformat__ = "google"
  14
  15__doc__ = """
  16Submodule to work with common format images.
  17
  18![](https://vedo.embl.es/images/basic/rotateImage.png)
  19"""
  20
  21__all__ = [
  22    "Image",
  23    "Picture",  # Deprecated, use Image instead
  24]
  25
  26
  27#################################################
  28def _get_img(obj: Union[np.ndarray, str], flip=False, translate=()) -> vtki.vtkImageData:
  29    # compute vtkImageData from numpy array or filename
  30
  31    if isinstance(obj, str):
  32        if "https://" in obj:
  33            obj = vedo.file_io.download(obj, verbose=False)
  34
  35        fname = obj.lower()
  36        if fname.endswith(".png"):
  37            picr = vtki.new("PNGReader")
  38        elif fname.endswith(".jpg") or fname.endswith(".jpeg"):
  39            picr = vtki.new("JPEGReader")
  40        elif fname.endswith(".bmp"):
  41            picr = vtki.new("BMPReader")
  42        elif fname.endswith(".tif") or fname.endswith(".tiff"):
  43            picr = vtki.new("TIFFReader")
  44            picr.SetOrientationType(vedo.settings.tiff_orientation_type)
  45        else:
  46            colors.printc("Cannot understand image format", obj, c="r")
  47            return vtki.vtkImageData()
  48        picr.SetFileName(obj)
  49        picr.Update()
  50        img = picr.GetOutput()
  51
  52    else:
  53        obj = np.asarray(obj)
  54
  55        if obj.ndim == 3:  # has shape (nx,ny, ncolor_alpha_chan)
  56            iac = vtki.new("ImageAppendComponents")
  57            nchan = obj.shape[2]  # get number of channels in inputimage (L/LA/RGB/RGBA)
  58            for i in range(nchan):
  59                if flip:
  60                    arr = np.flip(np.flip(obj[:, :, i], 0), 0).ravel()
  61                else:
  62                    arr = np.flip(obj[:, :, i], 0).ravel()
  63                arr = np.clip(arr, 0, 255)
  64                varb = utils.numpy2vtk(arr, dtype=np.uint8, name="RGBA")
  65                imgb = vtki.vtkImageData()
  66                imgb.SetDimensions(obj.shape[1], obj.shape[0], 1)
  67                imgb.GetPointData().AddArray(varb)
  68                imgb.GetPointData().SetActiveScalars("RGBA")
  69                iac.AddInputData(imgb)
  70            iac.Update()
  71            img = iac.GetOutput()
  72
  73        elif obj.ndim == 2:  # black and white
  74            if flip:
  75                arr = np.flip(obj[:, :], 0).ravel()
  76            else:
  77                arr = obj.ravel()
  78            arr = np.clip(arr, 0, 255)
  79            varb = utils.numpy2vtk(arr, dtype=np.uint8, name="RGBA")
  80            img = vtki.vtkImageData()
  81            img.SetDimensions(obj.shape[1], obj.shape[0], 1)
  82
  83            img.GetPointData().AddArray(varb)
  84            img.GetPointData().SetActiveScalars("RGBA")
  85
  86    if len(translate) > 0:
  87        translate_extent = vtki.new("ImageTranslateExtent")
  88        translate_extent.SetTranslation(-translate[0], -translate[1], 0)
  89        translate_extent.SetInputData(img)
  90        translate_extent.Update()
  91        img.DeepCopy(translate_extent.GetOutput())
  92
  93    return img
  94
  95
  96def _set_justification(img, pos):
  97
  98    if not isinstance(pos, str):
  99        return img, pos
 100
 101    sx, sy = img.GetDimensions()[:2]
 102    translate = ()
 103    if "top" in pos:
 104        if "left" in pos:
 105            pos = (0, 1)
 106            translate = (0, sy)
 107        elif "right" in pos:
 108            pos = (1, 1)
 109            translate = (sx, sy)
 110        elif "mid" in pos or "cent" in pos:
 111            pos = (0.5, 1)
 112            translate = (sx / 2, sy)
 113    elif "bottom" in pos:
 114        if "left" in pos:
 115            pos = (0, 0)
 116        elif "right" in pos:
 117            pos = (1, 0)
 118            translate = (sx, 0)
 119        elif "mid" in pos or "cent" in pos:
 120            pos = (0.5, 0)
 121            translate = (sx / 2, 0)
 122    elif "mid" in pos or "cent" in pos:
 123        if "left" in pos:
 124            pos = (0, 0.5)
 125            translate = (0, sy / 2)
 126        elif "right" in pos:
 127            pos = (1, 0.5)
 128            translate = (sx, sy / 2)
 129        else:
 130            pos = (0.5, 0.5)
 131            translate = (sx / 2, sy / 2)
 132
 133    if len(translate) > 0:
 134        translate = np.array(translate).astype(int)
 135        translate_extent = vtki.new("ImageTranslateExtent")
 136        translate_extent.SetTranslation(-translate[0], -translate[1], 0)
 137        translate_extent.SetInputData(img)
 138        translate_extent.Update()
 139        img = translate_extent.GetOutput()
 140
 141    return img, pos
 142
 143
 144class Image(vedo.visual.ImageVisual):
 145    """
 146    Class used to represent 2D images in a 3D world.
 147    """
 148
 149    def __init__(self, obj=None, channels=3):
 150        """
 151        Can be instantiated with a path file name or with a numpy array.
 152        Can also be instantiated with a matplotlib figure.
 153
 154        By default the transparency channel is disabled.
 155        To enable it set `channels=4`.
 156
 157        Use `Image.shape` to get the number of pixels in x and y.
 158
 159        Arguments:
 160            channels :  (int, list)
 161                only select these specific rgba channels (useful to remove alpha)
 162        """
 163        self.name = "Image"
 164        self.filename = ""
 165        self.file_size = 0
 166        self.pipeline = None
 167        self.time = 0
 168        self.rendered_at = set()
 169        self.info = {}
 170
 171        self.actor = vtki.vtkImageActor()
 172        self.actor.retrieve_object = weak_ref_to(self)
 173        self.properties = self.actor.GetProperty()
 174
 175        self.transform = vedo.LinearTransform()
 176
 177        if utils.is_sequence(obj) and len(obj) > 0:  # passing array
 178            img = _get_img(obj, False)
 179
 180        elif isinstance(obj, vtki.vtkImageData):
 181            img = obj
 182
 183        elif isinstance(obj, str):
 184            img = _get_img(obj)
 185            self.filename = obj
 186
 187        elif "matplotlib" in str(obj.__class__):
 188            fig = obj
 189            if hasattr(fig, "gcf"):
 190                fig = fig.gcf()
 191            fig.tight_layout(pad=1)
 192            fig.canvas.draw()
 193
 194            # self.array = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8)
 195            # self.array = self.array.reshape(fig.canvas.get_width_height()[::-1] + (3,))
 196            width, height = fig.get_size_inches() * fig.get_dpi()
 197            self.array = np.frombuffer(
 198                fig.canvas.buffer_rgba(), dtype=np.uint8
 199            ).reshape((int(height), int(width), 4))
 200            self.array = self.array[:, :, :3]
 201
 202            img = _get_img(self.array)
 203
 204        else:
 205            img = vtki.vtkImageData()
 206
 207        ############# select channels
 208        if isinstance(channels, int):
 209            channels = list(range(channels))
 210
 211        nchans = len(channels)
 212        n = img.GetPointData().GetScalars().GetNumberOfComponents()
 213        if nchans and n > nchans:
 214            pec = vtki.new("ImageExtractComponents")
 215            pec.SetInputData(img)
 216            if nchans == 4:
 217                pec.SetComponents(channels[0], channels[1], channels[2], channels[3])
 218            elif nchans == 3:
 219                pec.SetComponents(channels[0], channels[1], channels[2])
 220            elif nchans == 2:
 221                pec.SetComponents(channels[0], channels[1])
 222            elif nchans == 1:
 223                pec.SetComponents(channels[0])
 224            pec.Update()
 225            img = pec.GetOutput()
 226
 227        self.dataset = img
 228        self.actor.SetInputData(img)
 229        self.mapper = self.actor.GetMapper()
 230
 231        sx, sy, _ = self.dataset.GetDimensions()
 232        shape = np.array([sx, sy])
 233        self.pipeline = utils.OperationNode("Image", comment=f"#shape {shape}", c="#f28482")
 234    
 235    ######################################################################
 236
 237    def __str__(self):
 238        """Print a description of the Image class."""
 239
 240        module = self.__class__.__module__
 241        name = self.__class__.__name__
 242        out = vedo.printc(
 243            f"{module}.{name} at ({hex(id(self))})".ljust(75),
 244            c="y", bold=True, invert=True, return_string=True,
 245        )
 246
 247        # if vedo.colors._terminal_has_colors:
 248        #     thumb = ""
 249        #     try: # to generate a terminal thumbnail
 250        #         w = 75
 251        #         width, height = self.shape
 252        #         h = int(height / width * (w - 1) * 0.5 + 0.5)
 253        #         img_arr = self.clone().resize([w, h]).tonumpy()
 254        #         h, w = img_arr.shape[:2]
 255        #         for x in range(h):
 256        #             for y in range(w):
 257        #                 pix = img_arr[x][y]
 258        #                 r, g, b = pix[:3]
 259        #                 thumb += f"\x1b[48;2;{r};{g};{b}m "
 260        #             thumb += "\x1b[0m\n"
 261        #     except:
 262        #         pass
 263        #     out += thumb
 264        
 265        out += "\x1b[0m\x1b[33;1m"
 266        out += "dimensions".ljust(14) + f": {self.shape}\n"
 267        out += "memory size".ljust(14) + ": "
 268        out += str(int(self.memory_size())) + " kB\n"
 269
 270        bnds = self.bounds()
 271        bx1, bx2 = utils.precision(bnds[0], 3), utils.precision(bnds[1], 3)
 272        by1, by2 = utils.precision(bnds[2], 3), utils.precision(bnds[3], 3)
 273        bz1, bz2 = utils.precision(bnds[4], 3), utils.precision(bnds[5], 3)
 274        out += "position".ljust(14) + f": {self.pos()}\n"
 275        out += "bounds".ljust(14) + ":"
 276        out += " x=(" + bx1 + ", " + bx2 + "),"
 277        out += " y=(" + by1 + ", " + by2 + "),"
 278        out += " z=(" + bz1 + ", " + bz2 + ")\n"
 279        out += "intensty range".ljust(14) + f": {self.scalar_range()}\n"
 280        out += "level/window".ljust(14) + ": "
 281        out += str(self.level()) + " / " + str(self.window()) + "\n"
 282        return out.rstrip() + "\x1b[0m"
 283
 284    def _repr_html_(self):
 285        """
 286        HTML representation of the Image object for Jupyter Notebooks.
 287
 288        Returns:
 289            HTML text with the image and some properties.
 290        """
 291        import io
 292        import base64
 293        from PIL import Image
 294
 295        library_name = "vedo.image.Image"
 296        help_url = "https://vedo.embl.es/docs/vedo/image.html"
 297
 298        arr = self.thumbnail(zoom=1.1)
 299
 300        im = Image.fromarray(arr)
 301        buffered = io.BytesIO()
 302        im.save(buffered, format="PNG", quality=100)
 303        encoded = base64.b64encode(buffered.getvalue()).decode("utf-8")
 304        url = "data:image/png;base64," + encoded
 305        image = f"<img src='{url}'></img>"
 306
 307        help_text = ""
 308        if self.name:
 309            help_text += f"<b> {self.name}: &nbsp&nbsp</b>"
 310        help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>"
 311        if self.filename:
 312            dots = ""
 313            if len(self.filename) > 30:
 314                dots = "..."
 315            help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>"
 316
 317        pdata = ""
 318        if self.dataset.GetPointData().GetScalars():
 319            if self.dataset.GetPointData().GetScalars().GetName():
 320                name = self.dataset.GetPointData().GetScalars().GetName()
 321                pdata = "<tr><td><b> point data array </b></td><td>" + name + "</td></tr>"
 322
 323        cdata = ""
 324        if self.dataset.GetCellData().GetScalars():
 325            if self.dataset.GetCellData().GetScalars().GetName():
 326                name = self.dataset.GetCellData().GetScalars().GetName()
 327                cdata = "<tr><td><b> voxel data array </b></td><td>" + name + "</td></tr>"
 328
 329        img = self.dataset
 330
 331        allt = [
 332            "<table>",
 333            "<tr>",
 334            "<td>",
 335            image,
 336            "</td>",
 337            "<td style='text-align: center; vertical-align: center;'><br/>",
 338            help_text,
 339            "<table>",
 340            "<tr><td><b> shape </b></td><td>" + str(img.GetDimensions()[:2]) + "</td></tr>",
 341            "<tr><td><b> in memory size </b></td><td>"
 342            + str(int(img.GetActualMemorySize()))
 343            + " KB</td></tr>",
 344            pdata,
 345            cdata,
 346            "<tr><td><b> intensity range </b></td><td>" + str(img.GetScalarRange()) + "</td></tr>",
 347            "<tr><td><b> level&nbsp/&nbspwindow </b></td><td>"
 348            + str(self.level())
 349            + "&nbsp/&nbsp"
 350            + str(self.window())
 351            + "</td></tr>",
 352            "</table>",
 353            "</table>",
 354        ]
 355        return "\n".join(allt)
 356
 357    ######################################################################
 358    def _update(self, data: vtki.vtkImageData) -> Self:
 359        self.dataset = data
 360        self.mapper.SetInputData(data)
 361        self.mapper.Modified()
 362        return self
 363
 364    def dimensions(self) -> np.ndarray:
 365        """
 366        Return the image dimension as number of pixels in x and y. 
 367        Alias of property `shape`.
 368        """
 369        nx, ny, _ = self.dataset.GetDimensions()
 370        return np.array([nx, ny])
 371
 372    @property
 373    def shape(self) -> np.ndarray:
 374        """Return the image shape as number of pixels in x and y"""
 375        return np.array(self.dimensions())
 376
 377    @property
 378    def channels(self) -> int:
 379        """Return the number of channels in image"""
 380        return self.dataset.GetPointData().GetScalars().GetNumberOfComponents()
 381
 382    @property
 383    def extent(self) -> Tuple[int, int, int, int]:
 384        """Return the physical extent that the image spans."""
 385        return self.dataset.GetExtent()
 386    
 387    @extent.setter
 388    def extent(self, ext: Tuple[int, int, int, int]):
 389        """Set the physical extent that the image spans."""
 390        self.dataset.SetExtent(ext[0], ext[1], ext[2], ext[3], 0, 0)
 391        self.mapper.Modified()
 392
 393    def copy(self) -> "Image":
 394        """Return a copy of the image. Alias of `clone()`."""
 395        return self.clone()
 396
 397    def clone(self) -> "Image":
 398        """Return an exact copy of the input Image.
 399        If transform is True, it is given the same scaling and position."""
 400        img = vtki.vtkImageData()
 401        img.DeepCopy(self.dataset)
 402        pic = Image(img)
 403        pic.name = self.name
 404        pic.filename = self.filename
 405        pic.apply_transform(self.transform)
 406        pic.properties = vtki.vtkImageProperty()
 407        pic.properties.DeepCopy(self.properties)
 408        pic.actor.SetProperty(pic.properties)
 409        pic.pipeline = utils.OperationNode("clone", parents=[self], c="#f7dada", shape="diamond")
 410        return pic
 411    
 412    def clone2d(self, pos=(0, 0), size=1, justify="") -> "vedo.visual.Actor2D":
 413        """
 414        Embed an image as a static 2D image in the canvas.
 415        
 416        Return a 2D (an `Actor2D`) copy of the input Image.
 417        
 418        Arguments:
 419            pos : (list, str)
 420                2D (x,y) position in range [0,1],
 421                [0,0] being the bottom-left corner  
 422            size : (float)
 423                apply a scaling factor to the image
 424            justify : (str)
 425                define the anchor point ("top-left", "top-center", ...)
 426        """
 427        pic = vedo.visual.Actor2D()
 428
 429        pic.name = self.name
 430        pic.filename = self.filename
 431        pic.file_size = self.file_size
 432        
 433        pic.dataset = self.dataset
 434
 435        pic.properties = pic.GetProperty()
 436        pic.properties.SetDisplayLocationToBackground()
 437
 438        if size != 1:
 439            newsize = np.array(self.dataset.GetDimensions()[:2]) * size
 440            newsize = newsize.astype(int)
 441            rsz = vtki.new("ImageResize")
 442            rsz.SetInputData(self.dataset)
 443            rsz.SetResizeMethodToOutputDimensions()
 444            rsz.SetOutputDimensions(newsize[0], newsize[1], 1)
 445            rsz.Update()
 446            pic.dataset = rsz.GetOutput()
 447
 448        if justify:
 449            pic.dataset, pos = _set_justification(pic.dataset, justify)
 450        else:
 451            pic.dataset, pos = _set_justification(pic.dataset, pos)
 452
 453        pic.mapper = vtki.new("ImageMapper")
 454        # pic.SetMapper(pic.mapper)
 455        pic.mapper.SetInputData(pic.dataset)
 456        pic.mapper.SetColorWindow(255)
 457        pic.mapper.SetColorLevel(127.5)
 458
 459        pic.GetPositionCoordinate().SetCoordinateSystem(3)
 460        pic.SetPosition(pos)
 461
 462        pic.pipeline = utils.OperationNode("clone2d", parents=[self], c="#f7dada", shape="diamond")
 463        return pic
 464
 465
 466    def crop(self, top=None, bottom=None, right=None, left=None, pixels=False) -> Self:
 467        """
 468        Crop image.
 469
 470        Arguments:
 471            top : (float)
 472                fraction to crop from the top margin
 473            bottom : (float)
 474                fraction to crop from the bottom margin
 475            left : (float)
 476                fraction to crop from the left margin
 477            right : (float)
 478                fraction to crop from the right margin
 479            pixels : (bool)
 480                units are pixels
 481        """
 482        extractVOI = vtki.new("ExtractVOI")
 483        extractVOI.SetInputData(self.dataset)
 484        extractVOI.IncludeBoundaryOn()
 485
 486        d = self.dataset.GetDimensions()
 487        if pixels:
 488            extractVOI.SetVOI(left, d[0] - right - 1, bottom, d[1] - top - 1, 0, 0)
 489        else:
 490            bx0, bx1, by0, by1 = 0, d[0]-1, 0, d[1]-1
 491            if left is not None:   bx0 = int((d[0]-1)*left)
 492            if right is not None:  bx1 = int((d[0]-1)*(1-right))
 493            if bottom is not None: by0 = int((d[1]-1)*bottom)
 494            if top is not None:    by1 = int((d[1]-1)*(1-top))
 495            extractVOI.SetVOI(bx0, bx1, by0, by1, 0, 0)
 496        extractVOI.Update()
 497
 498        self._update(extractVOI.GetOutput())
 499        self.pipeline = utils.OperationNode(
 500            "crop", comment=f"shape={tuple(self.shape)}", parents=[self], c="#f28482"
 501        )
 502        return self
 503
 504    def pad(self, pixels=10, value=255) -> Self:
 505        """
 506        Add the specified number of pixels at the image borders.
 507        Pixels can be a list formatted as `[left, right, bottom, top]`.
 508
 509        Arguments:
 510            pixels : (int, list)
 511                number of pixels to be added (or a list of length 4)
 512            value : (int)
 513                intensity value (gray-scale color) of the padding
 514        """
 515        x0, x1, y0, y1, _z0, _z1 = self.dataset.GetExtent()
 516        pf = vtki.new("ImageConstantPad")
 517        pf.SetInputData(self.dataset)
 518        pf.SetConstant(value)
 519        if utils.is_sequence(pixels):
 520            pf.SetOutputWholeExtent(
 521                x0 - pixels[0], x1 + pixels[1],
 522                y0 - pixels[2], y1 + pixels[3],
 523                0, 0
 524            )
 525        else:
 526            pf.SetOutputWholeExtent(
 527                x0 - pixels, x1 + pixels,
 528                y0 - pixels, y1 + pixels,
 529                0, 0
 530            )
 531        pf.Update()
 532        self._update(pf.GetOutput())
 533        self.pipeline = utils.OperationNode(
 534            "pad", comment=f"{pixels} pixels", parents=[self], c="#f28482"
 535        )
 536        return self
 537
 538    def tile(self, nx=4, ny=4, shift=(0, 0)) -> "Image":
 539        """
 540        Generate a tiling from the current image by mirroring and repeating it.
 541
 542        Arguments:
 543            nx : (float)
 544                number of repeats along x
 545            ny : (float)
 546                number of repeats along x
 547            shift : (list)
 548                shift in x and y in pixels
 549        """
 550        x0, x1, y0, y1, z0, z1 = self.dataset.GetExtent()
 551        constant_pad = vtki.new("ImageMirrorPad")
 552        constant_pad.SetInputData(self.dataset)
 553        constant_pad.SetOutputWholeExtent(
 554            int(x0 + shift[0] + 0.5),
 555            int(x1 * nx + shift[0] + 0.5),
 556            int(y0 + shift[1] + 0.5),
 557            int(y1 * ny + shift[1] + 0.5),
 558            z0,
 559            z1,
 560        )
 561        constant_pad.Update()
 562        img = Image(constant_pad.GetOutput())
 563
 564        img.pipeline = utils.OperationNode(
 565            "tile", comment=f"by {nx}x{ny}", parents=[self], c="#f28482"
 566        )
 567        return img
 568
 569    def append(self, images: list, axis="z", preserve_extents=False) -> Self:
 570        """
 571        Append the input images to the current one along the specified axis.
 572        Except for the append axis, all inputs must have the same extent.
 573        All inputs must have the same number of scalar components.
 574        The output has the same origin and spacing as the first input.
 575        The origin and spacing of all other inputs are ignored.
 576        All inputs must have the same scalar type.
 577
 578        Arguments:
 579            axis : (int, str)
 580                axis expanded to hold the multiple images
 581            preserve_extents : (bool)
 582                if True, the extent of the inputs is used to place
 583                the image in the output. The whole extent of the output is the union of the input
 584                whole extents. Any portion of the output not covered by the inputs is set to zero.
 585                The origin and spacing is taken from the first input.
 586
 587        Example:
 588            ```python
 589            from vedo import Image, dataurl
 590            pic = Image(dataurl+'dog.jpg').pad()
 591            pic.append([pic, pic], axis='y')
 592            pic.append([pic, pic, pic], axis='x')
 593            pic.show(axes=1).close()
 594            ```
 595            ![](https://vedo.embl.es/images/feats/pict_append.png)
 596        """
 597        ima = vtki.new("ImageAppend")
 598        ima.SetInputData(self.dataset)
 599        for p in images:
 600            if isinstance(p, vtki.vtkImageData):
 601                ima.AddInputData(p)
 602            else:
 603                ima.AddInputData(p.dataset)
 604        ima.SetPreserveExtents(preserve_extents)
 605        if axis == "x":
 606            axis = 0
 607        elif axis == "y":
 608            axis = 1
 609        ima.SetAppendAxis(axis)
 610        ima.Update()
 611        self._update(ima.GetOutput())
 612        self.pipeline = utils.OperationNode(
 613            "append", comment=f"axis={axis}", parents=[self, *images], c="#f28482"
 614        )
 615        return self
 616
 617    def resize(self, newsize: Any) -> Self:
 618        """
 619        Resize the image resolution by specifying the number of pixels in width and height.
 620        If left to zero, it will be automatically calculated to keep the original aspect ratio.
 621
 622        `newsize` is the shape of image as [npx, npy], or it can be also expressed as a fraction.
 623        """
 624        old_dims = np.array(self.dataset.GetDimensions())
 625
 626        if not utils.is_sequence(newsize):
 627            newsize = (old_dims * newsize + 0.5).astype(int)
 628
 629        if not newsize[1]:
 630            ar = old_dims[1] / old_dims[0]
 631            newsize = [newsize[0], int(newsize[0] * ar + 0.5)]
 632        if not newsize[0]:
 633            ar = old_dims[0] / old_dims[1]
 634            newsize = [int(newsize[1] * ar + 0.5), newsize[1]]
 635        newsize = [newsize[0], newsize[1], old_dims[2]]
 636
 637        rsz = vtki.new("ImageResize")
 638        rsz.SetInputData(self.dataset)
 639        rsz.SetResizeMethodToOutputDimensions()
 640        rsz.SetOutputDimensions(newsize)
 641        rsz.Update()
 642        out = rsz.GetOutput()
 643        out.SetSpacing(1, 1, 1)
 644        self._update(out)
 645        self.pipeline = utils.OperationNode(
 646            "resize", comment=f"shape={tuple(self.shape)}", parents=[self], c="#f28482"
 647        )
 648        return self
 649
 650    def mirror(self, axis="x") -> Self:
 651        """Mirror image along x or y axis. Same as `flip()`."""
 652        ff = vtki.new("ImageFlip")
 653        ff.SetInputData(self.dataset)
 654        if axis.lower() == "x":
 655            ff.SetFilteredAxis(0)
 656        elif axis.lower() == "y":
 657            ff.SetFilteredAxis(1)
 658        else:
 659            colors.printc("Error in mirror(): mirror must be set to x or y.", c="r")
 660            raise RuntimeError()
 661        ff.Update()
 662        self._update(ff.GetOutput())
 663        self.pipeline = utils.OperationNode(f"mirror {axis}", parents=[self], c="#f28482")
 664        return self
 665
 666    def flip(self, axis="y") -> Self:
 667        """Mirror image along x or y axis. Same as `mirror()`."""
 668        return self.mirror(axis=axis)
 669
 670    def select(self, component: int) -> "Image":
 671        """Select one single component of the rgb image."""
 672        ec = vtki.new("ImageExtractComponents")
 673        ec.SetInputData(self.dataset)
 674        ec.SetComponents(component)
 675        ec.Update()
 676        pic = Image(ec.GetOutput())
 677        pic.pipeline = utils.OperationNode(
 678            "select", comment=f"component {component}", parents=[self], c="#f28482"
 679        )
 680        return pic
 681
 682    def bw(self) -> Self:
 683        """Make it black and white using luminance calibration."""
 684        n = self.dataset.GetPointData().GetNumberOfComponents()
 685        if n == 4:
 686            ecr = vtki.new("ImageExtractComponents")
 687            ecr.SetInputData(self.dataset)
 688            ecr.SetComponents(0, 1, 2)
 689            ecr.Update()
 690            img = ecr.GetOutput()
 691        else:
 692            img = self.dataset
 693
 694        ecr = vtki.new("ImageLuminance")
 695        ecr.SetInputData(img)
 696        ecr.Update()
 697        self._update(ecr.GetOutput())
 698        self.pipeline = utils.OperationNode("black&white", parents=[self], c="#f28482")
 699        return self
 700
 701    def smooth(self, sigma=3, radius=None) -> Self:
 702        """
 703        Smooth a `Image` with Gaussian kernel.
 704
 705        Arguments:
 706            sigma : (int)
 707                number of sigmas in pixel units
 708            radius : (float)
 709                how far out the gaussian kernel will go before being clamped to zero
 710        """
 711        gsf = vtki.new("ImageGaussianSmooth")
 712        gsf.SetDimensionality(2)
 713        gsf.SetInputData(self.dataset)
 714        if radius is not None:
 715            if utils.is_sequence(radius):
 716                gsf.SetRadiusFactors(radius[0], radius[1])
 717            else:
 718                gsf.SetRadiusFactor(radius)
 719
 720        if utils.is_sequence(sigma):
 721            gsf.SetStandardDeviations(sigma[0], sigma[1])
 722        else:
 723            gsf.SetStandardDeviation(sigma)
 724        gsf.Update()
 725        self._update(gsf.GetOutput())
 726        self.pipeline = utils.OperationNode(
 727            "smooth", comment=f"sigma={sigma}", parents=[self], c="#f28482"
 728        )
 729        return self
 730
 731    def median(self) -> Self:
 732        """
 733        Median filter that preserves thin lines and corners.
 734
 735        It operates on a 5x5 pixel neighborhood. It computes two values initially:
 736        the median of the + neighbors and the median of the x neighbors.
 737        It then computes the median of these two values plus the center pixel.
 738        This result of this second median is the output pixel value.
 739        """
 740        medf = vtki.new("ImageHybridMedian2D")
 741        medf.SetInputData(self.dataset)
 742        medf.Update()
 743        self._update(medf.GetOutput())
 744        self.pipeline = utils.OperationNode("median", parents=[self], c="#f28482")
 745        return self
 746
 747    def enhance(self) -> Self:
 748        """
 749        Enhance a b&w image using the laplacian, enhancing high-freq edges.
 750
 751        Example:
 752            ```python
 753            from vedo import *
 754            pic = Image(dataurl+'images/dog.jpg').bw()
 755            show(pic, pic.clone().enhance(), N=2, mode='image', zoom='tight')
 756            ```
 757            ![](https://vedo.embl.es/images/feats/pict_enhance.png)
 758        """
 759        img = self.dataset
 760        scalarRange = img.GetPointData().GetScalars().GetRange()
 761
 762        cast = vtki.new("ImageCast")
 763        cast.SetInputData(img)
 764        cast.SetOutputScalarTypeToDouble()
 765        cast.Update()
 766
 767        laplacian = vtki.new("ImageLaplacian")
 768        laplacian.SetInputData(cast.GetOutput())
 769        laplacian.SetDimensionality(2)
 770        laplacian.Update()
 771
 772        subtr = vtki.new("ImageMathematics")
 773        subtr.SetInputData(0, cast.GetOutput())
 774        subtr.SetInputData(1, laplacian.GetOutput())
 775        subtr.SetOperationToSubtract()
 776        subtr.Update()
 777
 778        color_window = scalarRange[1] - scalarRange[0]
 779        color_level = color_window / 2
 780        original_color = vtki.new("ImageMapToWindowLevelColors")
 781        original_color.SetWindow(color_window)
 782        original_color.SetLevel(color_level)
 783        original_color.SetInputData(subtr.GetOutput())
 784        original_color.Update()
 785        self._update(original_color.GetOutput())
 786
 787        self.pipeline = utils.OperationNode("enhance", parents=[self], c="#f28482")
 788        return self
 789
 790    def fft(self, mode="magnitude", logscale=12, center=True) -> "Image":
 791        """
 792        Fast Fourier transform of a image.
 793
 794        Arguments:
 795            logscale : (float)
 796                if non-zero, take the logarithm of the intensity and scale it by this factor.
 797            mode : (str)
 798                either [magnitude, real, imaginary, complex], compute the point array data accordingly.
 799            center : (bool)
 800                shift constant zero-frequency to the center of the image for display.
 801                (FFT converts spatial images into frequency space, but puts the zero frequency at the origin)
 802        """
 803        ffti = vtki.new("ImageFFT")
 804        ffti.SetInputData(self.dataset)
 805        ffti.Update()
 806
 807        if "mag" in mode:
 808            mag = vtki.new("ImageMagnitude")
 809            mag.SetInputData(ffti.GetOutput())
 810            mag.Update()
 811            out = mag.GetOutput()
 812        elif "real" in mode:
 813            erf = vtki.new("ImageExtractComponents")
 814            erf.SetInputData(ffti.GetOutput())
 815            erf.SetComponents(0)
 816            erf.Update()
 817            out = erf.GetOutput()
 818        elif "imaginary" in mode:
 819            eimf = vtki.new("ImageExtractComponents")
 820            eimf.SetInputData(ffti.GetOutput())
 821            eimf.SetComponents(1)
 822            eimf.Update()
 823            out = eimf.GetOutput()
 824        elif "complex" in mode:
 825            out = ffti.GetOutput()
 826        else:
 827            colors.printc("Error in fft(): unknown mode", mode)
 828            raise RuntimeError()
 829
 830        if center:
 831            center = vtki.new("ImageFourierCenter")
 832            center.SetInputData(out)
 833            center.Update()
 834            out = center.GetOutput()
 835
 836        if "complex" not in mode:
 837            if logscale:
 838                ils = vtki.new("ImageLogarithmicScale")
 839                ils.SetInputData(out)
 840                ils.SetConstant(logscale)
 841                ils.Update()
 842                out = ils.GetOutput()
 843
 844        pic = Image(out)
 845        pic.pipeline = utils.OperationNode("FFT", parents=[self], c="#f28482")
 846        return pic
 847
 848    def rfft(self, mode="magnitude") -> "Image":
 849        """Reverse Fast Fourier transform of a image."""
 850
 851        ffti = vtki.new("ImageRFFT")
 852        ffti.SetInputData(self.dataset)
 853        ffti.Update()
 854
 855        if "mag" in mode:
 856            mag = vtki.new("ImageMagnitude")
 857            mag.SetInputData(ffti.GetOutput())
 858            mag.Update()
 859            out = mag.GetOutput()
 860        elif "real" in mode:
 861            erf = vtki.new("ImageExtractComponents")
 862            erf.SetInputData(ffti.GetOutput())
 863            erf.SetComponents(0)
 864            erf.Update()
 865            out = erf.GetOutput()
 866        elif "imaginary" in mode:
 867            eimf = vtki.new("ImageExtractComponents")
 868            eimf.SetInputData(ffti.GetOutput())
 869            eimf.SetComponents(1)
 870            eimf.Update()
 871            out = eimf.GetOutput()
 872        elif "complex" in mode:
 873            out = ffti.GetOutput()
 874        else:
 875            colors.printc("Error in rfft(): unknown mode", mode)
 876            raise RuntimeError()
 877
 878        pic = Image(out)
 879        pic.pipeline = utils.OperationNode("rFFT", parents=[self], c="#f28482")
 880        return pic
 881
 882    def filterpass(self, lowcutoff=None, highcutoff=None, order=3) -> Self:
 883        """
 884        Low-pass and high-pass filtering become trivial in the frequency domain.
 885        A portion of the pixels/voxels are simply masked or attenuated.
 886        This function applies a high pass Butterworth filter that attenuates the
 887        frequency domain image with the function
 888
 889        The gradual attenuation of the filter is important.
 890        A simple high-pass filter would simply mask a set of pixels in the frequency domain,
 891        but the abrupt transition would cause a ringing effect in the spatial domain.
 892
 893        Arguments:
 894            lowcutoff : (list)
 895                the cutoff frequencies
 896            highcutoff : (list)
 897                the cutoff frequencies
 898            order : (int)
 899                order determines sharpness of the cutoff curve
 900        """
 901        # https://lorensen.github.io/VTKExamples/site/Cxx/ImageProcessing/IdealHighPass
 902        fft = vtki.new("ImageFFT")
 903        fft.SetInputData(self.dataset)
 904        fft.Update()
 905        out = fft.GetOutput()
 906
 907        if highcutoff:
 908            blp = vtki.new("ImageButterworthLowPass")
 909            blp.SetInputData(out)
 910            blp.SetCutOff(highcutoff)
 911            blp.SetOrder(order)
 912            blp.Update()
 913            out = blp.GetOutput()
 914
 915        if lowcutoff:
 916            bhp = vtki.new("ImageButterworthHighPass")
 917            bhp.SetInputData(out)
 918            bhp.SetCutOff(lowcutoff)
 919            bhp.SetOrder(order)
 920            bhp.Update()
 921            out = bhp.GetOutput()
 922
 923        rfft = vtki.new("ImageRFFT")
 924        rfft.SetInputData(out)
 925        rfft.Update()
 926
 927        ecomp = vtki.new("ImageExtractComponents")
 928        ecomp.SetInputData(rfft.GetOutput())
 929        ecomp.SetComponents(0)
 930        ecomp.Update()
 931
 932        caster = vtki.new("ImageCast")
 933        caster.SetOutputScalarTypeToUnsignedChar()
 934        caster.SetInputData(ecomp.GetOutput())
 935        caster.Update()
 936        self._update(caster.GetOutput())
 937        self.pipeline = utils.OperationNode("filterpass", parents=[self], c="#f28482")
 938        return self
 939
 940    def blend(self, pic, alpha1=0.5, alpha2=0.5) -> Self:
 941        """
 942        Take L, LA, RGB, or RGBA images as input and blends
 943        them according to the alpha values and/or the opacity setting for each input.
 944        """
 945        blf = vtki.new("ImageBlend")
 946        blf.AddInputData(self.dataset)
 947        blf.AddInputData(pic.dataset)
 948        blf.SetOpacity(0, alpha1)
 949        blf.SetOpacity(1, alpha2)
 950        blf.SetBlendModeToNormal()
 951        blf.Update()
 952        self._update(blf.GetOutput())
 953        self.pipeline = utils.OperationNode("blend", parents=[self, pic], c="#f28482")
 954        return self
 955
 956    def warp(
 957        self,
 958        source_pts=(),
 959        target_pts=(),
 960        transform=None,
 961        sigma=1,
 962        mirroring=False,
 963        bc="w",
 964        alpha=1,
 965    ) -> Self:
 966        """
 967        Warp an image using thin-plate splines.
 968
 969        Arguments:
 970            source_pts : (list)
 971                source points
 972            target_pts : (list)
 973                target points
 974            transform : (vtkTransform)
 975                a vtkTransform object can be supplied
 976            sigma : (float), optional
 977                stiffness of the interpolation
 978            mirroring : (bool)
 979                fill the margins with a reflection of the original image
 980            bc : (color)
 981                fill the margins with a solid color
 982            alpha : (float)
 983                opacity of the filled margins
 984        """
 985        if transform is None:
 986            # source and target must be filled
 987            transform = vtki.vtkThinPlateSplineTransform()
 988            transform.SetBasisToR2LogR()
 989
 990            parents = [self]
 991            if isinstance(source_pts, vedo.Points):
 992                parents.append(source_pts)
 993                source_pts = source_pts.vertices
 994            if isinstance(target_pts, vedo.Points):
 995                parents.append(target_pts)
 996                target_pts = target_pts.vertices
 997
 998            ns = len(source_pts)
 999            nt = len(target_pts)
1000            if ns != nt:
1001                colors.printc("Error in image.warp(): #source != #target points", ns, nt, c="r")
1002                raise RuntimeError()
1003
1004            ptsou = vtki.vtkPoints()
1005            ptsou.SetNumberOfPoints(ns)
1006
1007            pttar = vtki.vtkPoints()
1008            pttar.SetNumberOfPoints(nt)
1009
1010            for i in range(ns):
1011                p = source_pts[i]
1012                ptsou.SetPoint(i, [p[0], p[1], 0])
1013                p = target_pts[i]
1014                pttar.SetPoint(i, [p[0], p[1], 0])
1015
1016            transform.SetSigma(sigma)
1017            transform.SetSourceLandmarks(pttar)
1018            transform.SetTargetLandmarks(ptsou)
1019        else:
1020            # ignore source and target
1021            pass
1022
1023        reslice = vtki.new("ImageReslice")
1024        reslice.SetInputData(self.dataset)
1025        reslice.SetOutputDimensionality(2)
1026        reslice.SetResliceTransform(transform)
1027        reslice.SetInterpolationModeToCubic()
1028        reslice.SetMirror(mirroring)
1029        c = np.array(colors.get_color(bc)) * 255
1030        reslice.SetBackgroundColor([c[0], c[1], c[2], alpha * 255])
1031        reslice.Update()
1032        self._update(reslice.GetOutput())
1033        self.pipeline = utils.OperationNode("warp", parents=parents, c="#f28482")
1034        return self
1035
1036    def invert(self) -> Self:
1037        """
1038        Return an inverted image (inverted in each color channel).
1039        """
1040        rgb = self.tonumpy()
1041        data = 255 - np.array(rgb)
1042        self._update(_get_img(data))
1043        self.pipeline = utils.OperationNode("invert", parents=[self], c="#f28482")
1044        return self
1045
1046    def binarize(self, threshold=None, invert=False) -> Self:
1047        """
1048        Return a new Image where pixel above threshold are set to 255
1049        and pixels below are set to 0.
1050
1051        Arguments:
1052            threshold : (float)
1053                input threshold value
1054            invert : (bool)
1055                invert threshold direction
1056
1057        Example:
1058        ```python
1059        from vedo import Image, show
1060        pic1 = Image("https://aws.glamour.es/prod/designs/v1/assets/620x459/547577.jpg")
1061        pic2 = pic1.clone().invert()
1062        pic3 = pic1.clone().binarize()
1063        show(pic1, pic2, pic3, N=3, bg="blue9").close()
1064        ```
1065        ![](https://vedo.embl.es/images/feats/pict_binarize.png)
1066        """
1067        rgb = self.tonumpy()
1068        if rgb.ndim == 3:
1069            intensity = np.sum(rgb, axis=2) / 3
1070        else:
1071            intensity = rgb
1072
1073        if threshold is None:
1074            vmin, vmax = np.min(intensity), np.max(intensity)
1075            threshold = (vmax + vmin) / 2
1076
1077        data = np.zeros_like(intensity).astype(np.uint8)
1078        mask = np.where(intensity > threshold)
1079        if invert:
1080            data += 255
1081            data[mask] = 0
1082        else:
1083            data[mask] = 255
1084
1085        self._update(_get_img(data, flip=True))
1086
1087        self.pipeline = utils.OperationNode(
1088            "binarize", comment=f"threshold={threshold}", parents=[self], c="#f28482"
1089        )
1090        return self
1091
1092    def threshold(self, value=None, flip=False) -> "vedo.Mesh":
1093        """
1094        Create a polygonal Mesh from a Image by filling regions with pixels
1095        luminosity above a specified value.
1096
1097        Arguments:
1098            value : (float)
1099                The default is None, e.i. 1/3 of the scalar range.
1100            flip: (bool)
1101                Flip polygon orientations
1102
1103        Returns:
1104            A polygonal mesh.
1105        """
1106        mgf = vtki.new("ImageMagnitude")
1107        mgf.SetInputData(self.dataset)
1108        mgf.Update()
1109        msq = vtki.new("MarchingSquares")
1110        msq.SetInputData(mgf.GetOutput())
1111        if value is None:
1112            r0, r1 = self.dataset.GetScalarRange()
1113            value = r0 + (r1 - r0) / 3
1114        msq.SetValue(0, value)
1115        msq.Update()
1116        if flip:
1117            rs = vtki.new("ReverseSense")
1118            rs.SetInputData(msq.GetOutput())
1119            rs.ReverseCellsOn()
1120            rs.ReverseNormalsOff()
1121            rs.Update()
1122            output = rs.GetOutput()
1123        else:
1124            output = msq.GetOutput()
1125        ctr = vtki.new("ContourTriangulator")
1126        ctr.SetInputData(output)
1127        ctr.Update()
1128        out = vedo.Mesh(ctr.GetOutput(), c="k").bc("t").lighting("off")
1129
1130        out.pipeline = utils.OperationNode(
1131            "threshold", comment=f"{value: .2f}", parents=[self], c="#f28482:#e9c46a"
1132        )
1133        return out
1134
1135    def cmap(self, name: str, vmin=None, vmax=None) -> Self:
1136        """Colorize a image with a colormap representing pixel intensity"""
1137        n = self.dataset.GetPointData().GetNumberOfComponents()
1138        if n > 1:
1139            ecr = vtki.new("ImageExtractComponents")
1140            ecr.SetInputData(self.dataset)
1141            ecr.SetComponents(0, 1, 2)
1142            ecr.Update()
1143            ilum = vtki.new("ImageMagnitude")
1144            ilum.SetInputData(self.dataset)
1145            ilum.Update()
1146            img = ilum.GetOutput()
1147        else:
1148            img = self.dataset
1149
1150        lut = vtki.vtkLookupTable()
1151        _vmin, _vmax = img.GetScalarRange()
1152        if vmin is not None:
1153            _vmin = vmin
1154        if vmax is not None:
1155            _vmax = vmax
1156        lut.SetRange(_vmin, _vmax)
1157
1158        ncols = 256
1159        lut.SetNumberOfTableValues(ncols)
1160        cols = colors.color_map(range(ncols), name, 0, ncols)
1161        for i, c in enumerate(cols):
1162            lut.SetTableValue(i, *c)
1163        lut.Build()
1164
1165        imap = vtki.new("ImageMapToColors")
1166        imap.SetLookupTable(lut)
1167        imap.SetInputData(img)
1168        imap.Update()
1169        self._update(imap.GetOutput())
1170        self.pipeline = utils.OperationNode(
1171            "cmap", comment=f'"{name}"', parents=[self], c="#f28482"
1172        )
1173        return self
1174
1175    def rotate(self, angle: float, center=(), scale=1.0, mirroring=False, bc="w", alpha=1.0) -> Self:
1176        """
1177        Rotate by the specified angle (anticlockwise).
1178
1179        Arguments:
1180            angle : (float)
1181                rotation angle in degrees
1182            center : (list)
1183                center of rotation (x,y) in pixels
1184        """
1185        bounds = self.bounds()
1186        pc = [0, 0, 0]
1187        if center:
1188            pc[0] = center[0]
1189            pc[1] = center[1]
1190        else:
1191            pc[0] = (bounds[1] + bounds[0]) / 2.0
1192            pc[1] = (bounds[3] + bounds[2]) / 2.0
1193        pc[2] = (bounds[5] + bounds[4]) / 2.0
1194
1195        transform = vtki.vtkTransform()
1196        transform.Translate(pc)
1197        transform.RotateWXYZ(-angle, 0, 0, 1)
1198        transform.Scale(1 / scale, 1 / scale, 1)
1199        transform.Translate(-pc[0], -pc[1], -pc[2])
1200
1201        reslice = vtki.new("ImageReslice")
1202        reslice.SetMirror(mirroring)
1203        c = np.array(colors.get_color(bc)) * 255
1204        reslice.SetBackgroundColor([c[0], c[1], c[2], alpha * 255])
1205        reslice.SetInputData(self.dataset)
1206        reslice.SetResliceTransform(transform)
1207        reslice.SetOutputDimensionality(2)
1208        reslice.SetInterpolationModeToCubic()
1209        reslice.AutoCropOutputOn()
1210        reslice.Update()
1211        self._update(reslice.GetOutput())
1212
1213        self.pipeline = utils.OperationNode(
1214            "rotate", comment=f"angle={angle}", parents=[self], c="#f28482"
1215        )
1216        return self
1217
1218    def tomesh(self) -> "vedo.shapes.Grid":
1219        """
1220        Convert an image to polygonal data (quads),
1221        with each polygon vertex assigned a RGBA value.
1222        """
1223        dims = self.dataset.GetDimensions()
1224        gr = vedo.shapes.Grid(s=dims[:2], res=(dims[0] - 1, dims[1] - 1))
1225        gr.pos(int(dims[0] / 2), int(dims[1] / 2)).pickable(True).wireframe(False).lw(0)
1226        self.dataset.GetPointData().GetScalars().SetName("RGBA")
1227        gr.dataset.GetPointData().AddArray(self.dataset.GetPointData().GetScalars())
1228        gr.dataset.GetPointData().SetActiveScalars("RGBA")
1229        gr.mapper.SetArrayName("RGBA")
1230        gr.mapper.SetScalarModeToUsePointData()
1231        gr.mapper.ScalarVisibilityOn()
1232        gr.name = self.name
1233        gr.filename = self.filename
1234        gr.pipeline = utils.OperationNode("tomesh", parents=[self], c="#f28482:#e9c46a")
1235        return gr
1236
1237    def tonumpy(self) -> np.ndarray:
1238        """
1239        Get read-write access to pixels of a Image object as a numpy array.
1240        Note that the shape is (nrofchannels, nx, ny).
1241
1242        When you set values in the output image, you don't want numpy to reallocate the array
1243        but instead set values in the existing array, so use the [:] operator.
1244        Example: arr[:] = arr - 15
1245
1246        If the array is modified call:
1247        `image.modified()`
1248        when all your modifications are completed.
1249        """
1250        nx, ny, _ = self.dataset.GetDimensions()
1251        nchan = self.dataset.GetPointData().GetScalars().GetNumberOfComponents()
1252        narray = utils.vtk2numpy(self.dataset.GetPointData().GetScalars()).reshape(ny, nx, nchan)
1253        narray = np.flip(narray, axis=0).astype(np.uint8)
1254        return narray.squeeze()
1255
1256    def add_rectangle(self, xspan: List[float], yspan: List[float], c="green5", alpha=1.0) -> Self:
1257        """Draw a rectangle box on top of current image. Units are pixels.
1258
1259        Example:
1260            ```python
1261            import vedo
1262            pic = vedo.Image(vedo.dataurl+"images/dog.jpg")
1263            pic.add_rectangle([100,300], [100,200], c='green4', alpha=0.7)
1264            pic.add_line([100,100],[400,500], lw=2, alpha=1)
1265            pic.add_triangle([250,300], [100,300], [200,400], c='blue5')
1266            vedo.show(pic, axes=1).close()
1267            ```
1268            ![](https://vedo.embl.es/images/feats/pict_drawon.png)
1269        """
1270        x1, x2 = xspan
1271        y1, y2 = yspan
1272
1273        r, g, b = vedo.colors.get_color(c)
1274        c = np.array([r, g, b]) * 255
1275        c = c.astype(np.uint8)
1276
1277        alpha = min(alpha, 1)
1278        if alpha <= 0:
1279            return self
1280        alpha2 = alpha
1281        alpha1 = 1 - alpha
1282
1283        nx, ny = self.dimensions()
1284        if x2 > nx:
1285            x2 = nx - 1
1286        if y2 > ny:
1287            y2 = ny - 1
1288
1289        nchan = self.channels
1290        narrayA = self.tonumpy()
1291
1292        canvas_source = vtki.new("ImageCanvasSource2D")
1293        canvas_source.SetExtent(0, nx - 1, 0, ny - 1, 0, 0)
1294        canvas_source.SetScalarTypeToUnsignedChar()
1295        canvas_source.SetNumberOfScalarComponents(nchan)
1296        canvas_source.SetDrawColor(255, 255, 255)
1297        canvas_source.FillBox(x1, x2, y1, y2)
1298        canvas_source.Update()
1299        imagedataset = canvas_source.GetOutput()
1300
1301        vscals = imagedataset.GetPointData().GetScalars()
1302        narrayB = vedo.utils.vtk2numpy(vscals).reshape(ny, nx, nchan)
1303        narrayB = np.flip(narrayB, axis=0)
1304        narrayC = np.where(narrayB < 255, narrayA, alpha1 * narrayA + alpha2 * c)
1305        self._update(_get_img(narrayC))
1306        self.pipeline = utils.OperationNode("rectangle", parents=[self], c="#f28482")
1307        return self
1308
1309    def add_line(self, p1: List[float], p2: List[float], lw=2, c="k2", alpha=1.0) -> Self:
1310        """Draw a line on top of current image. Units are pixels."""
1311        x1, x2 = p1
1312        y1, y2 = p2
1313
1314        r, g, b = vedo.colors.get_color(c)
1315        c = np.array([r, g, b]) * 255
1316        c = c.astype(np.uint8)
1317
1318        alpha = min(alpha, 1)
1319        if alpha <= 0:
1320            return self
1321        alpha2 = alpha
1322        alpha1 = 1 - alpha
1323
1324        nx, ny = self.dimensions()
1325        if x2 > nx:
1326            x2 = nx - 1
1327        if y2 > ny:
1328            y2 = ny - 1
1329
1330        nchan = self.channels
1331        narrayA = self.tonumpy()
1332
1333        canvas_source = vtki.new("ImageCanvasSource2D")
1334        canvas_source.SetExtent(0, nx - 1, 0, ny - 1, 0, 0)
1335        canvas_source.SetScalarTypeToUnsignedChar()
1336        canvas_source.SetNumberOfScalarComponents(nchan)
1337        canvas_source.SetDrawColor(255, 255, 255)
1338        canvas_source.FillTube(x1, x2, y1, y2, lw)
1339        canvas_source.Update()
1340        imagedataset = canvas_source.GetOutput()
1341
1342        vscals = imagedataset.GetPointData().GetScalars()
1343        narrayB = vedo.utils.vtk2numpy(vscals).reshape(ny, nx, nchan)
1344        narrayB = np.flip(narrayB, axis=0)
1345        narrayC = np.where(narrayB < 255, narrayA, alpha1 * narrayA + alpha2 * c)
1346        self._update(_get_img(narrayC))
1347        self.pipeline = utils.OperationNode("line", parents=[self], c="#f28482")
1348        return self
1349
1350    def add_triangle(self, p1: List[float], p2: List[float], p3: List[float], c="red3", alpha=1.0) -> Self:
1351        """Draw a triangle on top of current image. Units are pixels."""
1352        x1, y1 = p1
1353        x2, y2 = p2
1354        x3, y3 = p3
1355
1356        r, g, b = vedo.colors.get_color(c)
1357        c = np.array([r, g, b]) * 255
1358        c = c.astype(np.uint8)
1359
1360        alpha = min(alpha, 1)
1361        if alpha <= 0:
1362            return self
1363        alpha2 = alpha
1364        alpha1 = 1 - alpha
1365
1366        nx, ny = self.dimensions()
1367        x1 = min(x1, nx)
1368        x2 = min(x2, nx)
1369        x3 = min(x3, nx)
1370
1371        y1 = min(y1, ny)
1372        y2 = min(y2, ny)
1373        y3 = min(y3, ny)
1374
1375        nchan = self.channels
1376        narrayA = self.tonumpy()
1377
1378        canvas_source = vtki.new("ImageCanvasSource2D")
1379        canvas_source.SetExtent(0, nx - 1, 0, ny - 1, 0, 0)
1380        canvas_source.SetScalarTypeToUnsignedChar()
1381        canvas_source.SetNumberOfScalarComponents(nchan)
1382        canvas_source.SetDrawColor(255, 255, 255)
1383        canvas_source.FillTriangle(x1, y1, x2, y2, x3, y3)
1384        canvas_source.Update()
1385        imagedataset = canvas_source.GetOutput()
1386
1387        vscals = imagedataset.GetPointData().GetScalars()
1388        narrayB = vedo.utils.vtk2numpy(vscals).reshape(ny, nx, nchan)
1389        narrayB = np.flip(narrayB, axis=0)
1390        narrayC = np.where(narrayB < 255, narrayA, alpha1 * narrayA + alpha2 * c)
1391        self._update(_get_img(narrayC))
1392        self.pipeline = utils.OperationNode("triangle", parents=[self], c="#f28482")
1393        return self
1394
1395    def add_text(
1396        self,
1397        txt: str,
1398        width=400,
1399        height=200,
1400        alpha=1,
1401        c="black",
1402        bg=None,
1403        alpha_bg=1,
1404        font="Theemim",
1405        dpi=200,
1406        justify="bottom-left",
1407    ) -> Self:
1408        """Add text to an image."""
1409
1410        tp = vtki.vtkTextProperty()
1411        tp.BoldOff()
1412        tp.FrameOff()
1413        tp.SetColor(colors.get_color(c))
1414        tp.SetJustificationToLeft()
1415        if "top" in justify:
1416            tp.SetVerticalJustificationToTop()
1417        if "bottom" in justify:
1418            tp.SetVerticalJustificationToBottom()
1419        if "cent" in justify:
1420            tp.SetVerticalJustificationToCentered()
1421            tp.SetJustificationToCentered()
1422        if "left" in justify:
1423            tp.SetJustificationToLeft()
1424        if "right" in justify:
1425            tp.SetJustificationToRight()
1426
1427        if   font.lower() == "courier": tp.SetFontFamilyToCourier()
1428        elif font.lower() == "times": tp.SetFontFamilyToTimes()
1429        elif font.lower() == "arial": tp.SetFontFamilyToArial()
1430        else:
1431            tp.SetFontFamily(vtki.VTK_FONT_FILE)
1432            tp.SetFontFile(utils.get_font_path(font))
1433
1434        if bg:
1435            bgcol = colors.get_color(bg)
1436            tp.SetBackgroundColor(bgcol)
1437            tp.SetBackgroundOpacity(alpha_bg)
1438            tp.SetFrameColor(bgcol)
1439            tp.FrameOn()
1440
1441        tr = vtki.new("TextRenderer")
1442        # GetConstrainedFontSize (const vtkUnicodeString &str,
1443        # vtkTextProperty(*tprop, int targetWidth, int targetHeight, int dpi)
1444        fs = tr.GetConstrainedFontSize(txt, tp, width, height, dpi)
1445        tp.SetFontSize(fs)
1446
1447        img = vtki.vtkImageData()
1448        # img.SetOrigin(*pos,1)
1449        tr.RenderString(tp, txt, img, [width, height], dpi)
1450        # RenderString (vtkTextProperty *tprop, const vtkStdString &str,
1451        #   vtkImageData *data, int textDims[2], int dpi, int backend=Default)
1452
1453        blf = vtki.new("ImageBlend")
1454        blf.AddInputData(self.dataset)
1455        blf.AddInputData(img)
1456        blf.SetOpacity(0, 1)
1457        blf.SetOpacity(1, alpha)
1458        blf.SetBlendModeToNormal()
1459        blf.Update()
1460
1461        self._update(blf.GetOutput())
1462        self.pipeline = utils.OperationNode(
1463            "add_text", comment=f"{txt}", parents=[self], c="#f28482"
1464        )
1465        return self
1466
1467    def modified(self) -> Self:
1468        """Use this method in conjunction with `tonumpy()`
1469        to update any modifications to the image array."""
1470        self.dataset.GetPointData().GetScalars().Modified()
1471        return self
1472
1473    def write(self, filename: str) -> Self:
1474        """Write image to file as png or jpg."""
1475        vedo.file_io.write(self, filename)
1476        self.pipeline = utils.OperationNode(
1477            "write",
1478            comment=filename[:15],
1479            parents=[self],
1480            c="#8a817c",
1481            shape="cylinder",
1482        )
1483        return self
1484
1485#################################################
1486class Picture(Image):
1487    def __init__(self, obj=None, channels=3):
1488        """Deprecated. Use `Image` instead."""
1489        vedo.logger.warning("Picture() is deprecated, use Image() instead.")
1490        super().__init__(obj=obj, channels=channels)
class Image(vedo.visual.ImageVisual):
 145class Image(vedo.visual.ImageVisual):
 146    """
 147    Class used to represent 2D images in a 3D world.
 148    """
 149
 150    def __init__(self, obj=None, channels=3):
 151        """
 152        Can be instantiated with a path file name or with a numpy array.
 153        Can also be instantiated with a matplotlib figure.
 154
 155        By default the transparency channel is disabled.
 156        To enable it set `channels=4`.
 157
 158        Use `Image.shape` to get the number of pixels in x and y.
 159
 160        Arguments:
 161            channels :  (int, list)
 162                only select these specific rgba channels (useful to remove alpha)
 163        """
 164        self.name = "Image"
 165        self.filename = ""
 166        self.file_size = 0
 167        self.pipeline = None
 168        self.time = 0
 169        self.rendered_at = set()
 170        self.info = {}
 171
 172        self.actor = vtki.vtkImageActor()
 173        self.actor.retrieve_object = weak_ref_to(self)
 174        self.properties = self.actor.GetProperty()
 175
 176        self.transform = vedo.LinearTransform()
 177
 178        if utils.is_sequence(obj) and len(obj) > 0:  # passing array
 179            img = _get_img(obj, False)
 180
 181        elif isinstance(obj, vtki.vtkImageData):
 182            img = obj
 183
 184        elif isinstance(obj, str):
 185            img = _get_img(obj)
 186            self.filename = obj
 187
 188        elif "matplotlib" in str(obj.__class__):
 189            fig = obj
 190            if hasattr(fig, "gcf"):
 191                fig = fig.gcf()
 192            fig.tight_layout(pad=1)
 193            fig.canvas.draw()
 194
 195            # self.array = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8)
 196            # self.array = self.array.reshape(fig.canvas.get_width_height()[::-1] + (3,))
 197            width, height = fig.get_size_inches() * fig.get_dpi()
 198            self.array = np.frombuffer(
 199                fig.canvas.buffer_rgba(), dtype=np.uint8
 200            ).reshape((int(height), int(width), 4))
 201            self.array = self.array[:, :, :3]
 202
 203            img = _get_img(self.array)
 204
 205        else:
 206            img = vtki.vtkImageData()
 207
 208        ############# select channels
 209        if isinstance(channels, int):
 210            channels = list(range(channels))
 211
 212        nchans = len(channels)
 213        n = img.GetPointData().GetScalars().GetNumberOfComponents()
 214        if nchans and n > nchans:
 215            pec = vtki.new("ImageExtractComponents")
 216            pec.SetInputData(img)
 217            if nchans == 4:
 218                pec.SetComponents(channels[0], channels[1], channels[2], channels[3])
 219            elif nchans == 3:
 220                pec.SetComponents(channels[0], channels[1], channels[2])
 221            elif nchans == 2:
 222                pec.SetComponents(channels[0], channels[1])
 223            elif nchans == 1:
 224                pec.SetComponents(channels[0])
 225            pec.Update()
 226            img = pec.GetOutput()
 227
 228        self.dataset = img
 229        self.actor.SetInputData(img)
 230        self.mapper = self.actor.GetMapper()
 231
 232        sx, sy, _ = self.dataset.GetDimensions()
 233        shape = np.array([sx, sy])
 234        self.pipeline = utils.OperationNode("Image", comment=f"#shape {shape}", c="#f28482")
 235    
 236    ######################################################################
 237
 238    def __str__(self):
 239        """Print a description of the Image class."""
 240
 241        module = self.__class__.__module__
 242        name = self.__class__.__name__
 243        out = vedo.printc(
 244            f"{module}.{name} at ({hex(id(self))})".ljust(75),
 245            c="y", bold=True, invert=True, return_string=True,
 246        )
 247
 248        # if vedo.colors._terminal_has_colors:
 249        #     thumb = ""
 250        #     try: # to generate a terminal thumbnail
 251        #         w = 75
 252        #         width, height = self.shape
 253        #         h = int(height / width * (w - 1) * 0.5 + 0.5)
 254        #         img_arr = self.clone().resize([w, h]).tonumpy()
 255        #         h, w = img_arr.shape[:2]
 256        #         for x in range(h):
 257        #             for y in range(w):
 258        #                 pix = img_arr[x][y]
 259        #                 r, g, b = pix[:3]
 260        #                 thumb += f"\x1b[48;2;{r};{g};{b}m "
 261        #             thumb += "\x1b[0m\n"
 262        #     except:
 263        #         pass
 264        #     out += thumb
 265        
 266        out += "\x1b[0m\x1b[33;1m"
 267        out += "dimensions".ljust(14) + f": {self.shape}\n"
 268        out += "memory size".ljust(14) + ": "
 269        out += str(int(self.memory_size())) + " kB\n"
 270
 271        bnds = self.bounds()
 272        bx1, bx2 = utils.precision(bnds[0], 3), utils.precision(bnds[1], 3)
 273        by1, by2 = utils.precision(bnds[2], 3), utils.precision(bnds[3], 3)
 274        bz1, bz2 = utils.precision(bnds[4], 3), utils.precision(bnds[5], 3)
 275        out += "position".ljust(14) + f": {self.pos()}\n"
 276        out += "bounds".ljust(14) + ":"
 277        out += " x=(" + bx1 + ", " + bx2 + "),"
 278        out += " y=(" + by1 + ", " + by2 + "),"
 279        out += " z=(" + bz1 + ", " + bz2 + ")\n"
 280        out += "intensty range".ljust(14) + f": {self.scalar_range()}\n"
 281        out += "level/window".ljust(14) + ": "
 282        out += str(self.level()) + " / " + str(self.window()) + "\n"
 283        return out.rstrip() + "\x1b[0m"
 284
 285    def _repr_html_(self):
 286        """
 287        HTML representation of the Image object for Jupyter Notebooks.
 288
 289        Returns:
 290            HTML text with the image and some properties.
 291        """
 292        import io
 293        import base64
 294        from PIL import Image
 295
 296        library_name = "vedo.image.Image"
 297        help_url = "https://vedo.embl.es/docs/vedo/image.html"
 298
 299        arr = self.thumbnail(zoom=1.1)
 300
 301        im = Image.fromarray(arr)
 302        buffered = io.BytesIO()
 303        im.save(buffered, format="PNG", quality=100)
 304        encoded = base64.b64encode(buffered.getvalue()).decode("utf-8")
 305        url = "data:image/png;base64," + encoded
 306        image = f"<img src='{url}'></img>"
 307
 308        help_text = ""
 309        if self.name:
 310            help_text += f"<b> {self.name}: &nbsp&nbsp</b>"
 311        help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>"
 312        if self.filename:
 313            dots = ""
 314            if len(self.filename) > 30:
 315                dots = "..."
 316            help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>"
 317
 318        pdata = ""
 319        if self.dataset.GetPointData().GetScalars():
 320            if self.dataset.GetPointData().GetScalars().GetName():
 321                name = self.dataset.GetPointData().GetScalars().GetName()
 322                pdata = "<tr><td><b> point data array </b></td><td>" + name + "</td></tr>"
 323
 324        cdata = ""
 325        if self.dataset.GetCellData().GetScalars():
 326            if self.dataset.GetCellData().GetScalars().GetName():
 327                name = self.dataset.GetCellData().GetScalars().GetName()
 328                cdata = "<tr><td><b> voxel data array </b></td><td>" + name + "</td></tr>"
 329
 330        img = self.dataset
 331
 332        allt = [
 333            "<table>",
 334            "<tr>",
 335            "<td>",
 336            image,
 337            "</td>",
 338            "<td style='text-align: center; vertical-align: center;'><br/>",
 339            help_text,
 340            "<table>",
 341            "<tr><td><b> shape </b></td><td>" + str(img.GetDimensions()[:2]) + "</td></tr>",
 342            "<tr><td><b> in memory size </b></td><td>"
 343            + str(int(img.GetActualMemorySize()))
 344            + " KB</td></tr>",
 345            pdata,
 346            cdata,
 347            "<tr><td><b> intensity range </b></td><td>" + str(img.GetScalarRange()) + "</td></tr>",
 348            "<tr><td><b> level&nbsp/&nbspwindow </b></td><td>"
 349            + str(self.level())
 350            + "&nbsp/&nbsp"
 351            + str(self.window())
 352            + "</td></tr>",
 353            "</table>",
 354            "</table>",
 355        ]
 356        return "\n".join(allt)
 357
 358    ######################################################################
 359    def _update(self, data: vtki.vtkImageData) -> Self:
 360        self.dataset = data
 361        self.mapper.SetInputData(data)
 362        self.mapper.Modified()
 363        return self
 364
 365    def dimensions(self) -> np.ndarray:
 366        """
 367        Return the image dimension as number of pixels in x and y. 
 368        Alias of property `shape`.
 369        """
 370        nx, ny, _ = self.dataset.GetDimensions()
 371        return np.array([nx, ny])
 372
 373    @property
 374    def shape(self) -> np.ndarray:
 375        """Return the image shape as number of pixels in x and y"""
 376        return np.array(self.dimensions())
 377
 378    @property
 379    def channels(self) -> int:
 380        """Return the number of channels in image"""
 381        return self.dataset.GetPointData().GetScalars().GetNumberOfComponents()
 382
 383    @property
 384    def extent(self) -> Tuple[int, int, int, int]:
 385        """Return the physical extent that the image spans."""
 386        return self.dataset.GetExtent()
 387    
 388    @extent.setter
 389    def extent(self, ext: Tuple[int, int, int, int]):
 390        """Set the physical extent that the image spans."""
 391        self.dataset.SetExtent(ext[0], ext[1], ext[2], ext[3], 0, 0)
 392        self.mapper.Modified()
 393
 394    def copy(self) -> "Image":
 395        """Return a copy of the image. Alias of `clone()`."""
 396        return self.clone()
 397
 398    def clone(self) -> "Image":
 399        """Return an exact copy of the input Image.
 400        If transform is True, it is given the same scaling and position."""
 401        img = vtki.vtkImageData()
 402        img.DeepCopy(self.dataset)
 403        pic = Image(img)
 404        pic.name = self.name
 405        pic.filename = self.filename
 406        pic.apply_transform(self.transform)
 407        pic.properties = vtki.vtkImageProperty()
 408        pic.properties.DeepCopy(self.properties)
 409        pic.actor.SetProperty(pic.properties)
 410        pic.pipeline = utils.OperationNode("clone", parents=[self], c="#f7dada", shape="diamond")
 411        return pic
 412    
 413    def clone2d(self, pos=(0, 0), size=1, justify="") -> "vedo.visual.Actor2D":
 414        """
 415        Embed an image as a static 2D image in the canvas.
 416        
 417        Return a 2D (an `Actor2D`) copy of the input Image.
 418        
 419        Arguments:
 420            pos : (list, str)
 421                2D (x,y) position in range [0,1],
 422                [0,0] being the bottom-left corner  
 423            size : (float)
 424                apply a scaling factor to the image
 425            justify : (str)
 426                define the anchor point ("top-left", "top-center", ...)
 427        """
 428        pic = vedo.visual.Actor2D()
 429
 430        pic.name = self.name
 431        pic.filename = self.filename
 432        pic.file_size = self.file_size
 433        
 434        pic.dataset = self.dataset
 435
 436        pic.properties = pic.GetProperty()
 437        pic.properties.SetDisplayLocationToBackground()
 438
 439        if size != 1:
 440            newsize = np.array(self.dataset.GetDimensions()[:2]) * size
 441            newsize = newsize.astype(int)
 442            rsz = vtki.new("ImageResize")
 443            rsz.SetInputData(self.dataset)
 444            rsz.SetResizeMethodToOutputDimensions()
 445            rsz.SetOutputDimensions(newsize[0], newsize[1], 1)
 446            rsz.Update()
 447            pic.dataset = rsz.GetOutput()
 448
 449        if justify:
 450            pic.dataset, pos = _set_justification(pic.dataset, justify)
 451        else:
 452            pic.dataset, pos = _set_justification(pic.dataset, pos)
 453
 454        pic.mapper = vtki.new("ImageMapper")
 455        # pic.SetMapper(pic.mapper)
 456        pic.mapper.SetInputData(pic.dataset)
 457        pic.mapper.SetColorWindow(255)
 458        pic.mapper.SetColorLevel(127.5)
 459
 460        pic.GetPositionCoordinate().SetCoordinateSystem(3)
 461        pic.SetPosition(pos)
 462
 463        pic.pipeline = utils.OperationNode("clone2d", parents=[self], c="#f7dada", shape="diamond")
 464        return pic
 465
 466
 467    def crop(self, top=None, bottom=None, right=None, left=None, pixels=False) -> Self:
 468        """
 469        Crop image.
 470
 471        Arguments:
 472            top : (float)
 473                fraction to crop from the top margin
 474            bottom : (float)
 475                fraction to crop from the bottom margin
 476            left : (float)
 477                fraction to crop from the left margin
 478            right : (float)
 479                fraction to crop from the right margin
 480            pixels : (bool)
 481                units are pixels
 482        """
 483        extractVOI = vtki.new("ExtractVOI")
 484        extractVOI.SetInputData(self.dataset)
 485        extractVOI.IncludeBoundaryOn()
 486
 487        d = self.dataset.GetDimensions()
 488        if pixels:
 489            extractVOI.SetVOI(left, d[0] - right - 1, bottom, d[1] - top - 1, 0, 0)
 490        else:
 491            bx0, bx1, by0, by1 = 0, d[0]-1, 0, d[1]-1
 492            if left is not None:   bx0 = int((d[0]-1)*left)
 493            if right is not None:  bx1 = int((d[0]-1)*(1-right))
 494            if bottom is not None: by0 = int((d[1]-1)*bottom)
 495            if top is not None:    by1 = int((d[1]-1)*(1-top))
 496            extractVOI.SetVOI(bx0, bx1, by0, by1, 0, 0)
 497        extractVOI.Update()
 498
 499        self._update(extractVOI.GetOutput())
 500        self.pipeline = utils.OperationNode(
 501            "crop", comment=f"shape={tuple(self.shape)}", parents=[self], c="#f28482"
 502        )
 503        return self
 504
 505    def pad(self, pixels=10, value=255) -> Self:
 506        """
 507        Add the specified number of pixels at the image borders.
 508        Pixels can be a list formatted as `[left, right, bottom, top]`.
 509
 510        Arguments:
 511            pixels : (int, list)
 512                number of pixels to be added (or a list of length 4)
 513            value : (int)
 514                intensity value (gray-scale color) of the padding
 515        """
 516        x0, x1, y0, y1, _z0, _z1 = self.dataset.GetExtent()
 517        pf = vtki.new("ImageConstantPad")
 518        pf.SetInputData(self.dataset)
 519        pf.SetConstant(value)
 520        if utils.is_sequence(pixels):
 521            pf.SetOutputWholeExtent(
 522                x0 - pixels[0], x1 + pixels[1],
 523                y0 - pixels[2], y1 + pixels[3],
 524                0, 0
 525            )
 526        else:
 527            pf.SetOutputWholeExtent(
 528                x0 - pixels, x1 + pixels,
 529                y0 - pixels, y1 + pixels,
 530                0, 0
 531            )
 532        pf.Update()
 533        self._update(pf.GetOutput())
 534        self.pipeline = utils.OperationNode(
 535            "pad", comment=f"{pixels} pixels", parents=[self], c="#f28482"
 536        )
 537        return self
 538
 539    def tile(self, nx=4, ny=4, shift=(0, 0)) -> "Image":
 540        """
 541        Generate a tiling from the current image by mirroring and repeating it.
 542
 543        Arguments:
 544            nx : (float)
 545                number of repeats along x
 546            ny : (float)
 547                number of repeats along x
 548            shift : (list)
 549                shift in x and y in pixels
 550        """
 551        x0, x1, y0, y1, z0, z1 = self.dataset.GetExtent()
 552        constant_pad = vtki.new("ImageMirrorPad")
 553        constant_pad.SetInputData(self.dataset)
 554        constant_pad.SetOutputWholeExtent(
 555            int(x0 + shift[0] + 0.5),
 556            int(x1 * nx + shift[0] + 0.5),
 557            int(y0 + shift[1] + 0.5),
 558            int(y1 * ny + shift[1] + 0.5),
 559            z0,
 560            z1,
 561        )
 562        constant_pad.Update()
 563        img = Image(constant_pad.GetOutput())
 564
 565        img.pipeline = utils.OperationNode(
 566            "tile", comment=f"by {nx}x{ny}", parents=[self], c="#f28482"
 567        )
 568        return img
 569
 570    def append(self, images: list, axis="z", preserve_extents=False) -> Self:
 571        """
 572        Append the input images to the current one along the specified axis.
 573        Except for the append axis, all inputs must have the same extent.
 574        All inputs must have the same number of scalar components.
 575        The output has the same origin and spacing as the first input.
 576        The origin and spacing of all other inputs are ignored.
 577        All inputs must have the same scalar type.
 578
 579        Arguments:
 580            axis : (int, str)
 581                axis expanded to hold the multiple images
 582            preserve_extents : (bool)
 583                if True, the extent of the inputs is used to place
 584                the image in the output. The whole extent of the output is the union of the input
 585                whole extents. Any portion of the output not covered by the inputs is set to zero.
 586                The origin and spacing is taken from the first input.
 587
 588        Example:
 589            ```python
 590            from vedo import Image, dataurl
 591            pic = Image(dataurl+'dog.jpg').pad()
 592            pic.append([pic, pic], axis='y')
 593            pic.append([pic, pic, pic], axis='x')
 594            pic.show(axes=1).close()
 595            ```
 596            ![](https://vedo.embl.es/images/feats/pict_append.png)
 597        """
 598        ima = vtki.new("ImageAppend")
 599        ima.SetInputData(self.dataset)
 600        for p in images:
 601            if isinstance(p, vtki.vtkImageData):
 602                ima.AddInputData(p)
 603            else:
 604                ima.AddInputData(p.dataset)
 605        ima.SetPreserveExtents(preserve_extents)
 606        if axis == "x":
 607            axis = 0
 608        elif axis == "y":
 609            axis = 1
 610        ima.SetAppendAxis(axis)
 611        ima.Update()
 612        self._update(ima.GetOutput())
 613        self.pipeline = utils.OperationNode(
 614            "append", comment=f"axis={axis}", parents=[self, *images], c="#f28482"
 615        )
 616        return self
 617
 618    def resize(self, newsize: Any) -> Self:
 619        """
 620        Resize the image resolution by specifying the number of pixels in width and height.
 621        If left to zero, it will be automatically calculated to keep the original aspect ratio.
 622
 623        `newsize` is the shape of image as [npx, npy], or it can be also expressed as a fraction.
 624        """
 625        old_dims = np.array(self.dataset.GetDimensions())
 626
 627        if not utils.is_sequence(newsize):
 628            newsize = (old_dims * newsize + 0.5).astype(int)
 629
 630        if not newsize[1]:
 631            ar = old_dims[1] / old_dims[0]
 632            newsize = [newsize[0], int(newsize[0] * ar + 0.5)]
 633        if not newsize[0]:
 634            ar = old_dims[0] / old_dims[1]
 635            newsize = [int(newsize[1] * ar + 0.5), newsize[1]]
 636        newsize = [newsize[0], newsize[1], old_dims[2]]
 637
 638        rsz = vtki.new("ImageResize")
 639        rsz.SetInputData(self.dataset)
 640        rsz.SetResizeMethodToOutputDimensions()
 641        rsz.SetOutputDimensions(newsize)
 642        rsz.Update()
 643        out = rsz.GetOutput()
 644        out.SetSpacing(1, 1, 1)
 645        self._update(out)
 646        self.pipeline = utils.OperationNode(
 647            "resize", comment=f"shape={tuple(self.shape)}", parents=[self], c="#f28482"
 648        )
 649        return self
 650
 651    def mirror(self, axis="x") -> Self:
 652        """Mirror image along x or y axis. Same as `flip()`."""
 653        ff = vtki.new("ImageFlip")
 654        ff.SetInputData(self.dataset)
 655        if axis.lower() == "x":
 656            ff.SetFilteredAxis(0)
 657        elif axis.lower() == "y":
 658            ff.SetFilteredAxis(1)
 659        else:
 660            colors.printc("Error in mirror(): mirror must be set to x or y.", c="r")
 661            raise RuntimeError()
 662        ff.Update()
 663        self._update(ff.GetOutput())
 664        self.pipeline = utils.OperationNode(f"mirror {axis}", parents=[self], c="#f28482")
 665        return self
 666
 667    def flip(self, axis="y") -> Self:
 668        """Mirror image along x or y axis. Same as `mirror()`."""
 669        return self.mirror(axis=axis)
 670
 671    def select(self, component: int) -> "Image":
 672        """Select one single component of the rgb image."""
 673        ec = vtki.new("ImageExtractComponents")
 674        ec.SetInputData(self.dataset)
 675        ec.SetComponents(component)
 676        ec.Update()
 677        pic = Image(ec.GetOutput())
 678        pic.pipeline = utils.OperationNode(
 679            "select", comment=f"component {component}", parents=[self], c="#f28482"
 680        )
 681        return pic
 682
 683    def bw(self) -> Self:
 684        """Make it black and white using luminance calibration."""
 685        n = self.dataset.GetPointData().GetNumberOfComponents()
 686        if n == 4:
 687            ecr = vtki.new("ImageExtractComponents")
 688            ecr.SetInputData(self.dataset)
 689            ecr.SetComponents(0, 1, 2)
 690            ecr.Update()
 691            img = ecr.GetOutput()
 692        else:
 693            img = self.dataset
 694
 695        ecr = vtki.new("ImageLuminance")
 696        ecr.SetInputData(img)
 697        ecr.Update()
 698        self._update(ecr.GetOutput())
 699        self.pipeline = utils.OperationNode("black&white", parents=[self], c="#f28482")
 700        return self
 701
 702    def smooth(self, sigma=3, radius=None) -> Self:
 703        """
 704        Smooth a `Image` with Gaussian kernel.
 705
 706        Arguments:
 707            sigma : (int)
 708                number of sigmas in pixel units
 709            radius : (float)
 710                how far out the gaussian kernel will go before being clamped to zero
 711        """
 712        gsf = vtki.new("ImageGaussianSmooth")
 713        gsf.SetDimensionality(2)
 714        gsf.SetInputData(self.dataset)
 715        if radius is not None:
 716            if utils.is_sequence(radius):
 717                gsf.SetRadiusFactors(radius[0], radius[1])
 718            else:
 719                gsf.SetRadiusFactor(radius)
 720
 721        if utils.is_sequence(sigma):
 722            gsf.SetStandardDeviations(sigma[0], sigma[1])
 723        else:
 724            gsf.SetStandardDeviation(sigma)
 725        gsf.Update()
 726        self._update(gsf.GetOutput())
 727        self.pipeline = utils.OperationNode(
 728            "smooth", comment=f"sigma={sigma}", parents=[self], c="#f28482"
 729        )
 730        return self
 731
 732    def median(self) -> Self:
 733        """
 734        Median filter that preserves thin lines and corners.
 735
 736        It operates on a 5x5 pixel neighborhood. It computes two values initially:
 737        the median of the + neighbors and the median of the x neighbors.
 738        It then computes the median of these two values plus the center pixel.
 739        This result of this second median is the output pixel value.
 740        """
 741        medf = vtki.new("ImageHybridMedian2D")
 742        medf.SetInputData(self.dataset)
 743        medf.Update()
 744        self._update(medf.GetOutput())
 745        self.pipeline = utils.OperationNode("median", parents=[self], c="#f28482")
 746        return self
 747
 748    def enhance(self) -> Self:
 749        """
 750        Enhance a b&w image using the laplacian, enhancing high-freq edges.
 751
 752        Example:
 753            ```python
 754            from vedo import *
 755            pic = Image(dataurl+'images/dog.jpg').bw()
 756            show(pic, pic.clone().enhance(), N=2, mode='image', zoom='tight')
 757            ```
 758            ![](https://vedo.embl.es/images/feats/pict_enhance.png)
 759        """
 760        img = self.dataset
 761        scalarRange = img.GetPointData().GetScalars().GetRange()
 762
 763        cast = vtki.new("ImageCast")
 764        cast.SetInputData(img)
 765        cast.SetOutputScalarTypeToDouble()
 766        cast.Update()
 767
 768        laplacian = vtki.new("ImageLaplacian")
 769        laplacian.SetInputData(cast.GetOutput())
 770        laplacian.SetDimensionality(2)
 771        laplacian.Update()
 772
 773        subtr = vtki.new("ImageMathematics")
 774        subtr.SetInputData(0, cast.GetOutput())
 775        subtr.SetInputData(1, laplacian.GetOutput())
 776        subtr.SetOperationToSubtract()
 777        subtr.Update()
 778
 779        color_window = scalarRange[1] - scalarRange[0]
 780        color_level = color_window / 2
 781        original_color = vtki.new("ImageMapToWindowLevelColors")
 782        original_color.SetWindow(color_window)
 783        original_color.SetLevel(color_level)
 784        original_color.SetInputData(subtr.GetOutput())
 785        original_color.Update()
 786        self._update(original_color.GetOutput())
 787
 788        self.pipeline = utils.OperationNode("enhance", parents=[self], c="#f28482")
 789        return self
 790
 791    def fft(self, mode="magnitude", logscale=12, center=True) -> "Image":
 792        """
 793        Fast Fourier transform of a image.
 794
 795        Arguments:
 796            logscale : (float)
 797                if non-zero, take the logarithm of the intensity and scale it by this factor.
 798            mode : (str)
 799                either [magnitude, real, imaginary, complex], compute the point array data accordingly.
 800            center : (bool)
 801                shift constant zero-frequency to the center of the image for display.
 802                (FFT converts spatial images into frequency space, but puts the zero frequency at the origin)
 803        """
 804        ffti = vtki.new("ImageFFT")
 805        ffti.SetInputData(self.dataset)
 806        ffti.Update()
 807
 808        if "mag" in mode:
 809            mag = vtki.new("ImageMagnitude")
 810            mag.SetInputData(ffti.GetOutput())
 811            mag.Update()
 812            out = mag.GetOutput()
 813        elif "real" in mode:
 814            erf = vtki.new("ImageExtractComponents")
 815            erf.SetInputData(ffti.GetOutput())
 816            erf.SetComponents(0)
 817            erf.Update()
 818            out = erf.GetOutput()
 819        elif "imaginary" in mode:
 820            eimf = vtki.new("ImageExtractComponents")
 821            eimf.SetInputData(ffti.GetOutput())
 822            eimf.SetComponents(1)
 823            eimf.Update()
 824            out = eimf.GetOutput()
 825        elif "complex" in mode:
 826            out = ffti.GetOutput()
 827        else:
 828            colors.printc("Error in fft(): unknown mode", mode)
 829            raise RuntimeError()
 830
 831        if center:
 832            center = vtki.new("ImageFourierCenter")
 833            center.SetInputData(out)
 834            center.Update()
 835            out = center.GetOutput()
 836
 837        if "complex" not in mode:
 838            if logscale:
 839                ils = vtki.new("ImageLogarithmicScale")
 840                ils.SetInputData(out)
 841                ils.SetConstant(logscale)
 842                ils.Update()
 843                out = ils.GetOutput()
 844
 845        pic = Image(out)
 846        pic.pipeline = utils.OperationNode("FFT", parents=[self], c="#f28482")
 847        return pic
 848
 849    def rfft(self, mode="magnitude") -> "Image":
 850        """Reverse Fast Fourier transform of a image."""
 851
 852        ffti = vtki.new("ImageRFFT")
 853        ffti.SetInputData(self.dataset)
 854        ffti.Update()
 855
 856        if "mag" in mode:
 857            mag = vtki.new("ImageMagnitude")
 858            mag.SetInputData(ffti.GetOutput())
 859            mag.Update()
 860            out = mag.GetOutput()
 861        elif "real" in mode:
 862            erf = vtki.new("ImageExtractComponents")
 863            erf.SetInputData(ffti.GetOutput())
 864            erf.SetComponents(0)
 865            erf.Update()
 866            out = erf.GetOutput()
 867        elif "imaginary" in mode:
 868            eimf = vtki.new("ImageExtractComponents")
 869            eimf.SetInputData(ffti.GetOutput())
 870            eimf.SetComponents(1)
 871            eimf.Update()
 872            out = eimf.GetOutput()
 873        elif "complex" in mode:
 874            out = ffti.GetOutput()
 875        else:
 876            colors.printc("Error in rfft(): unknown mode", mode)
 877            raise RuntimeError()
 878
 879        pic = Image(out)
 880        pic.pipeline = utils.OperationNode("rFFT", parents=[self], c="#f28482")
 881        return pic
 882
 883    def filterpass(self, lowcutoff=None, highcutoff=None, order=3) -> Self:
 884        """
 885        Low-pass and high-pass filtering become trivial in the frequency domain.
 886        A portion of the pixels/voxels are simply masked or attenuated.
 887        This function applies a high pass Butterworth filter that attenuates the
 888        frequency domain image with the function
 889
 890        The gradual attenuation of the filter is important.
 891        A simple high-pass filter would simply mask a set of pixels in the frequency domain,
 892        but the abrupt transition would cause a ringing effect in the spatial domain.
 893
 894        Arguments:
 895            lowcutoff : (list)
 896                the cutoff frequencies
 897            highcutoff : (list)
 898                the cutoff frequencies
 899            order : (int)
 900                order determines sharpness of the cutoff curve
 901        """
 902        # https://lorensen.github.io/VTKExamples/site/Cxx/ImageProcessing/IdealHighPass
 903        fft = vtki.new("ImageFFT")
 904        fft.SetInputData(self.dataset)
 905        fft.Update()
 906        out = fft.GetOutput()
 907
 908        if highcutoff:
 909            blp = vtki.new("ImageButterworthLowPass")
 910            blp.SetInputData(out)
 911            blp.SetCutOff(highcutoff)
 912            blp.SetOrder(order)
 913            blp.Update()
 914            out = blp.GetOutput()
 915
 916        if lowcutoff:
 917            bhp = vtki.new("ImageButterworthHighPass")
 918            bhp.SetInputData(out)
 919            bhp.SetCutOff(lowcutoff)
 920            bhp.SetOrder(order)
 921            bhp.Update()
 922            out = bhp.GetOutput()
 923
 924        rfft = vtki.new("ImageRFFT")
 925        rfft.SetInputData(out)
 926        rfft.Update()
 927
 928        ecomp = vtki.new("ImageExtractComponents")
 929        ecomp.SetInputData(rfft.GetOutput())
 930        ecomp.SetComponents(0)
 931        ecomp.Update()
 932
 933        caster = vtki.new("ImageCast")
 934        caster.SetOutputScalarTypeToUnsignedChar()
 935        caster.SetInputData(ecomp.GetOutput())
 936        caster.Update()
 937        self._update(caster.GetOutput())
 938        self.pipeline = utils.OperationNode("filterpass", parents=[self], c="#f28482")
 939        return self
 940
 941    def blend(self, pic, alpha1=0.5, alpha2=0.5) -> Self:
 942        """
 943        Take L, LA, RGB, or RGBA images as input and blends
 944        them according to the alpha values and/or the opacity setting for each input.
 945        """
 946        blf = vtki.new("ImageBlend")
 947        blf.AddInputData(self.dataset)
 948        blf.AddInputData(pic.dataset)
 949        blf.SetOpacity(0, alpha1)
 950        blf.SetOpacity(1, alpha2)
 951        blf.SetBlendModeToNormal()
 952        blf.Update()
 953        self._update(blf.GetOutput())
 954        self.pipeline = utils.OperationNode("blend", parents=[self, pic], c="#f28482")
 955        return self
 956
 957    def warp(
 958        self,
 959        source_pts=(),
 960        target_pts=(),
 961        transform=None,
 962        sigma=1,
 963        mirroring=False,
 964        bc="w",
 965        alpha=1,
 966    ) -> Self:
 967        """
 968        Warp an image using thin-plate splines.
 969
 970        Arguments:
 971            source_pts : (list)
 972                source points
 973            target_pts : (list)
 974                target points
 975            transform : (vtkTransform)
 976                a vtkTransform object can be supplied
 977            sigma : (float), optional
 978                stiffness of the interpolation
 979            mirroring : (bool)
 980                fill the margins with a reflection of the original image
 981            bc : (color)
 982                fill the margins with a solid color
 983            alpha : (float)
 984                opacity of the filled margins
 985        """
 986        if transform is None:
 987            # source and target must be filled
 988            transform = vtki.vtkThinPlateSplineTransform()
 989            transform.SetBasisToR2LogR()
 990
 991            parents = [self]
 992            if isinstance(source_pts, vedo.Points):
 993                parents.append(source_pts)
 994                source_pts = source_pts.vertices
 995            if isinstance(target_pts, vedo.Points):
 996                parents.append(target_pts)
 997                target_pts = target_pts.vertices
 998
 999            ns = len(source_pts)
1000            nt = len(target_pts)
1001            if ns != nt:
1002                colors.printc("Error in image.warp(): #source != #target points", ns, nt, c="r")
1003                raise RuntimeError()
1004
1005            ptsou = vtki.vtkPoints()
1006            ptsou.SetNumberOfPoints(ns)
1007
1008            pttar = vtki.vtkPoints()
1009            pttar.SetNumberOfPoints(nt)
1010
1011            for i in range(ns):
1012                p = source_pts[i]
1013                ptsou.SetPoint(i, [p[0], p[1], 0])
1014                p = target_pts[i]
1015                pttar.SetPoint(i, [p[0], p[1], 0])
1016
1017            transform.SetSigma(sigma)
1018            transform.SetSourceLandmarks(pttar)
1019            transform.SetTargetLandmarks(ptsou)
1020        else:
1021            # ignore source and target
1022            pass
1023
1024        reslice = vtki.new("ImageReslice")
1025        reslice.SetInputData(self.dataset)
1026        reslice.SetOutputDimensionality(2)
1027        reslice.SetResliceTransform(transform)
1028        reslice.SetInterpolationModeToCubic()
1029        reslice.SetMirror(mirroring)
1030        c = np.array(colors.get_color(bc)) * 255
1031        reslice.SetBackgroundColor([c[0], c[1], c[2], alpha * 255])
1032        reslice.Update()
1033        self._update(reslice.GetOutput())
1034        self.pipeline = utils.OperationNode("warp", parents=parents, c="#f28482")
1035        return self
1036
1037    def invert(self) -> Self:
1038        """
1039        Return an inverted image (inverted in each color channel).
1040        """
1041        rgb = self.tonumpy()
1042        data = 255 - np.array(rgb)
1043        self._update(_get_img(data))
1044        self.pipeline = utils.OperationNode("invert", parents=[self], c="#f28482")
1045        return self
1046
1047    def binarize(self, threshold=None, invert=False) -> Self:
1048        """
1049        Return a new Image where pixel above threshold are set to 255
1050        and pixels below are set to 0.
1051
1052        Arguments:
1053            threshold : (float)
1054                input threshold value
1055            invert : (bool)
1056                invert threshold direction
1057
1058        Example:
1059        ```python
1060        from vedo import Image, show
1061        pic1 = Image("https://aws.glamour.es/prod/designs/v1/assets/620x459/547577.jpg")
1062        pic2 = pic1.clone().invert()
1063        pic3 = pic1.clone().binarize()
1064        show(pic1, pic2, pic3, N=3, bg="blue9").close()
1065        ```
1066        ![](https://vedo.embl.es/images/feats/pict_binarize.png)
1067        """
1068        rgb = self.tonumpy()
1069        if rgb.ndim == 3:
1070            intensity = np.sum(rgb, axis=2) / 3
1071        else:
1072            intensity = rgb
1073
1074        if threshold is None:
1075            vmin, vmax = np.min(intensity), np.max(intensity)
1076            threshold = (vmax + vmin) / 2
1077
1078        data = np.zeros_like(intensity).astype(np.uint8)
1079        mask = np.where(intensity > threshold)
1080        if invert:
1081            data += 255
1082            data[mask] = 0
1083        else:
1084            data[mask] = 255
1085
1086        self._update(_get_img(data, flip=True))
1087
1088        self.pipeline = utils.OperationNode(
1089            "binarize", comment=f"threshold={threshold}", parents=[self], c="#f28482"
1090        )
1091        return self
1092
1093    def threshold(self, value=None, flip=False) -> "vedo.Mesh":
1094        """
1095        Create a polygonal Mesh from a Image by filling regions with pixels
1096        luminosity above a specified value.
1097
1098        Arguments:
1099            value : (float)
1100                The default is None, e.i. 1/3 of the scalar range.
1101            flip: (bool)
1102                Flip polygon orientations
1103
1104        Returns:
1105            A polygonal mesh.
1106        """
1107        mgf = vtki.new("ImageMagnitude")
1108        mgf.SetInputData(self.dataset)
1109        mgf.Update()
1110        msq = vtki.new("MarchingSquares")
1111        msq.SetInputData(mgf.GetOutput())
1112        if value is None:
1113            r0, r1 = self.dataset.GetScalarRange()
1114            value = r0 + (r1 - r0) / 3
1115        msq.SetValue(0, value)
1116        msq.Update()
1117        if flip:
1118            rs = vtki.new("ReverseSense")
1119            rs.SetInputData(msq.GetOutput())
1120            rs.ReverseCellsOn()
1121            rs.ReverseNormalsOff()
1122            rs.Update()
1123            output = rs.GetOutput()
1124        else:
1125            output = msq.GetOutput()
1126        ctr = vtki.new("ContourTriangulator")
1127        ctr.SetInputData(output)
1128        ctr.Update()
1129        out = vedo.Mesh(ctr.GetOutput(), c="k").bc("t").lighting("off")
1130
1131        out.pipeline = utils.OperationNode(
1132            "threshold", comment=f"{value: .2f}", parents=[self], c="#f28482:#e9c46a"
1133        )
1134        return out
1135
1136    def cmap(self, name: str, vmin=None, vmax=None) -> Self:
1137        """Colorize a image with a colormap representing pixel intensity"""
1138        n = self.dataset.GetPointData().GetNumberOfComponents()
1139        if n > 1:
1140            ecr = vtki.new("ImageExtractComponents")
1141            ecr.SetInputData(self.dataset)
1142            ecr.SetComponents(0, 1, 2)
1143            ecr.Update()
1144            ilum = vtki.new("ImageMagnitude")
1145            ilum.SetInputData(self.dataset)
1146            ilum.Update()
1147            img = ilum.GetOutput()
1148        else:
1149            img = self.dataset
1150
1151        lut = vtki.vtkLookupTable()
1152        _vmin, _vmax = img.GetScalarRange()
1153        if vmin is not None:
1154            _vmin = vmin
1155        if vmax is not None:
1156            _vmax = vmax
1157        lut.SetRange(_vmin, _vmax)
1158
1159        ncols = 256
1160        lut.SetNumberOfTableValues(ncols)
1161        cols = colors.color_map(range(ncols), name, 0, ncols)
1162        for i, c in enumerate(cols):
1163            lut.SetTableValue(i, *c)
1164        lut.Build()
1165
1166        imap = vtki.new("ImageMapToColors")
1167        imap.SetLookupTable(lut)
1168        imap.SetInputData(img)
1169        imap.Update()
1170        self._update(imap.GetOutput())
1171        self.pipeline = utils.OperationNode(
1172            "cmap", comment=f'"{name}"', parents=[self], c="#f28482"
1173        )
1174        return self
1175
1176    def rotate(self, angle: float, center=(), scale=1.0, mirroring=False, bc="w", alpha=1.0) -> Self:
1177        """
1178        Rotate by the specified angle (anticlockwise).
1179
1180        Arguments:
1181            angle : (float)
1182                rotation angle in degrees
1183            center : (list)
1184                center of rotation (x,y) in pixels
1185        """
1186        bounds = self.bounds()
1187        pc = [0, 0, 0]
1188        if center:
1189            pc[0] = center[0]
1190            pc[1] = center[1]
1191        else:
1192            pc[0] = (bounds[1] + bounds[0]) / 2.0
1193            pc[1] = (bounds[3] + bounds[2]) / 2.0
1194        pc[2] = (bounds[5] + bounds[4]) / 2.0
1195
1196        transform = vtki.vtkTransform()
1197        transform.Translate(pc)
1198        transform.RotateWXYZ(-angle, 0, 0, 1)
1199        transform.Scale(1 / scale, 1 / scale, 1)
1200        transform.Translate(-pc[0], -pc[1], -pc[2])
1201
1202        reslice = vtki.new("ImageReslice")
1203        reslice.SetMirror(mirroring)
1204        c = np.array(colors.get_color(bc)) * 255
1205        reslice.SetBackgroundColor([c[0], c[1], c[2], alpha * 255])
1206        reslice.SetInputData(self.dataset)
1207        reslice.SetResliceTransform(transform)
1208        reslice.SetOutputDimensionality(2)
1209        reslice.SetInterpolationModeToCubic()
1210        reslice.AutoCropOutputOn()
1211        reslice.Update()
1212        self._update(reslice.GetOutput())
1213
1214        self.pipeline = utils.OperationNode(
1215            "rotate", comment=f"angle={angle}", parents=[self], c="#f28482"
1216        )
1217        return self
1218
1219    def tomesh(self) -> "vedo.shapes.Grid":
1220        """
1221        Convert an image to polygonal data (quads),
1222        with each polygon vertex assigned a RGBA value.
1223        """
1224        dims = self.dataset.GetDimensions()
1225        gr = vedo.shapes.Grid(s=dims[:2], res=(dims[0] - 1, dims[1] - 1))
1226        gr.pos(int(dims[0] / 2), int(dims[1] / 2)).pickable(True).wireframe(False).lw(0)
1227        self.dataset.GetPointData().GetScalars().SetName("RGBA")
1228        gr.dataset.GetPointData().AddArray(self.dataset.GetPointData().GetScalars())
1229        gr.dataset.GetPointData().SetActiveScalars("RGBA")
1230        gr.mapper.SetArrayName("RGBA")
1231        gr.mapper.SetScalarModeToUsePointData()
1232        gr.mapper.ScalarVisibilityOn()
1233        gr.name = self.name
1234        gr.filename = self.filename
1235        gr.pipeline = utils.OperationNode("tomesh", parents=[self], c="#f28482:#e9c46a")
1236        return gr
1237
1238    def tonumpy(self) -> np.ndarray:
1239        """
1240        Get read-write access to pixels of a Image object as a numpy array.
1241        Note that the shape is (nrofchannels, nx, ny).
1242
1243        When you set values in the output image, you don't want numpy to reallocate the array
1244        but instead set values in the existing array, so use the [:] operator.
1245        Example: arr[:] = arr - 15
1246
1247        If the array is modified call:
1248        `image.modified()`
1249        when all your modifications are completed.
1250        """
1251        nx, ny, _ = self.dataset.GetDimensions()
1252        nchan = self.dataset.GetPointData().GetScalars().GetNumberOfComponents()
1253        narray = utils.vtk2numpy(self.dataset.GetPointData().GetScalars()).reshape(ny, nx, nchan)
1254        narray = np.flip(narray, axis=0).astype(np.uint8)
1255        return narray.squeeze()
1256
1257    def add_rectangle(self, xspan: List[float], yspan: List[float], c="green5", alpha=1.0) -> Self:
1258        """Draw a rectangle box on top of current image. Units are pixels.
1259
1260        Example:
1261            ```python
1262            import vedo
1263            pic = vedo.Image(vedo.dataurl+"images/dog.jpg")
1264            pic.add_rectangle([100,300], [100,200], c='green4', alpha=0.7)
1265            pic.add_line([100,100],[400,500], lw=2, alpha=1)
1266            pic.add_triangle([250,300], [100,300], [200,400], c='blue5')
1267            vedo.show(pic, axes=1).close()
1268            ```
1269            ![](https://vedo.embl.es/images/feats/pict_drawon.png)
1270        """
1271        x1, x2 = xspan
1272        y1, y2 = yspan
1273
1274        r, g, b = vedo.colors.get_color(c)
1275        c = np.array([r, g, b]) * 255
1276        c = c.astype(np.uint8)
1277
1278        alpha = min(alpha, 1)
1279        if alpha <= 0:
1280            return self
1281        alpha2 = alpha
1282        alpha1 = 1 - alpha
1283
1284        nx, ny = self.dimensions()
1285        if x2 > nx:
1286            x2 = nx - 1
1287        if y2 > ny:
1288            y2 = ny - 1
1289
1290        nchan = self.channels
1291        narrayA = self.tonumpy()
1292
1293        canvas_source = vtki.new("ImageCanvasSource2D")
1294        canvas_source.SetExtent(0, nx - 1, 0, ny - 1, 0, 0)
1295        canvas_source.SetScalarTypeToUnsignedChar()
1296        canvas_source.SetNumberOfScalarComponents(nchan)
1297        canvas_source.SetDrawColor(255, 255, 255)
1298        canvas_source.FillBox(x1, x2, y1, y2)
1299        canvas_source.Update()
1300        imagedataset = canvas_source.GetOutput()
1301
1302        vscals = imagedataset.GetPointData().GetScalars()
1303        narrayB = vedo.utils.vtk2numpy(vscals).reshape(ny, nx, nchan)
1304        narrayB = np.flip(narrayB, axis=0)
1305        narrayC = np.where(narrayB < 255, narrayA, alpha1 * narrayA + alpha2 * c)
1306        self._update(_get_img(narrayC))
1307        self.pipeline = utils.OperationNode("rectangle", parents=[self], c="#f28482")
1308        return self
1309
1310    def add_line(self, p1: List[float], p2: List[float], lw=2, c="k2", alpha=1.0) -> Self:
1311        """Draw a line on top of current image. Units are pixels."""
1312        x1, x2 = p1
1313        y1, y2 = p2
1314
1315        r, g, b = vedo.colors.get_color(c)
1316        c = np.array([r, g, b]) * 255
1317        c = c.astype(np.uint8)
1318
1319        alpha = min(alpha, 1)
1320        if alpha <= 0:
1321            return self
1322        alpha2 = alpha
1323        alpha1 = 1 - alpha
1324
1325        nx, ny = self.dimensions()
1326        if x2 > nx:
1327            x2 = nx - 1
1328        if y2 > ny:
1329            y2 = ny - 1
1330
1331        nchan = self.channels
1332        narrayA = self.tonumpy()
1333
1334        canvas_source = vtki.new("ImageCanvasSource2D")
1335        canvas_source.SetExtent(0, nx - 1, 0, ny - 1, 0, 0)
1336        canvas_source.SetScalarTypeToUnsignedChar()
1337        canvas_source.SetNumberOfScalarComponents(nchan)
1338        canvas_source.SetDrawColor(255, 255, 255)
1339        canvas_source.FillTube(x1, x2, y1, y2, lw)
1340        canvas_source.Update()
1341        imagedataset = canvas_source.GetOutput()
1342
1343        vscals = imagedataset.GetPointData().GetScalars()
1344        narrayB = vedo.utils.vtk2numpy(vscals).reshape(ny, nx, nchan)
1345        narrayB = np.flip(narrayB, axis=0)
1346        narrayC = np.where(narrayB < 255, narrayA, alpha1 * narrayA + alpha2 * c)
1347        self._update(_get_img(narrayC))
1348        self.pipeline = utils.OperationNode("line", parents=[self], c="#f28482")
1349        return self
1350
1351    def add_triangle(self, p1: List[float], p2: List[float], p3: List[float], c="red3", alpha=1.0) -> Self:
1352        """Draw a triangle on top of current image. Units are pixels."""
1353        x1, y1 = p1
1354        x2, y2 = p2
1355        x3, y3 = p3
1356
1357        r, g, b = vedo.colors.get_color(c)
1358        c = np.array([r, g, b]) * 255
1359        c = c.astype(np.uint8)
1360
1361        alpha = min(alpha, 1)
1362        if alpha <= 0:
1363            return self
1364        alpha2 = alpha
1365        alpha1 = 1 - alpha
1366
1367        nx, ny = self.dimensions()
1368        x1 = min(x1, nx)
1369        x2 = min(x2, nx)
1370        x3 = min(x3, nx)
1371
1372        y1 = min(y1, ny)
1373        y2 = min(y2, ny)
1374        y3 = min(y3, ny)
1375
1376        nchan = self.channels
1377        narrayA = self.tonumpy()
1378
1379        canvas_source = vtki.new("ImageCanvasSource2D")
1380        canvas_source.SetExtent(0, nx - 1, 0, ny - 1, 0, 0)
1381        canvas_source.SetScalarTypeToUnsignedChar()
1382        canvas_source.SetNumberOfScalarComponents(nchan)
1383        canvas_source.SetDrawColor(255, 255, 255)
1384        canvas_source.FillTriangle(x1, y1, x2, y2, x3, y3)
1385        canvas_source.Update()
1386        imagedataset = canvas_source.GetOutput()
1387
1388        vscals = imagedataset.GetPointData().GetScalars()
1389        narrayB = vedo.utils.vtk2numpy(vscals).reshape(ny, nx, nchan)
1390        narrayB = np.flip(narrayB, axis=0)
1391        narrayC = np.where(narrayB < 255, narrayA, alpha1 * narrayA + alpha2 * c)
1392        self._update(_get_img(narrayC))
1393        self.pipeline = utils.OperationNode("triangle", parents=[self], c="#f28482")
1394        return self
1395
1396    def add_text(
1397        self,
1398        txt: str,
1399        width=400,
1400        height=200,
1401        alpha=1,
1402        c="black",
1403        bg=None,
1404        alpha_bg=1,
1405        font="Theemim",
1406        dpi=200,
1407        justify="bottom-left",
1408    ) -> Self:
1409        """Add text to an image."""
1410
1411        tp = vtki.vtkTextProperty()
1412        tp.BoldOff()
1413        tp.FrameOff()
1414        tp.SetColor(colors.get_color(c))
1415        tp.SetJustificationToLeft()
1416        if "top" in justify:
1417            tp.SetVerticalJustificationToTop()
1418        if "bottom" in justify:
1419            tp.SetVerticalJustificationToBottom()
1420        if "cent" in justify:
1421            tp.SetVerticalJustificationToCentered()
1422            tp.SetJustificationToCentered()
1423        if "left" in justify:
1424            tp.SetJustificationToLeft()
1425        if "right" in justify:
1426            tp.SetJustificationToRight()
1427
1428        if   font.lower() == "courier": tp.SetFontFamilyToCourier()
1429        elif font.lower() == "times": tp.SetFontFamilyToTimes()
1430        elif font.lower() == "arial": tp.SetFontFamilyToArial()
1431        else:
1432            tp.SetFontFamily(vtki.VTK_FONT_FILE)
1433            tp.SetFontFile(utils.get_font_path(font))
1434
1435        if bg:
1436            bgcol = colors.get_color(bg)
1437            tp.SetBackgroundColor(bgcol)
1438            tp.SetBackgroundOpacity(alpha_bg)
1439            tp.SetFrameColor(bgcol)
1440            tp.FrameOn()
1441
1442        tr = vtki.new("TextRenderer")
1443        # GetConstrainedFontSize (const vtkUnicodeString &str,
1444        # vtkTextProperty(*tprop, int targetWidth, int targetHeight, int dpi)
1445        fs = tr.GetConstrainedFontSize(txt, tp, width, height, dpi)
1446        tp.SetFontSize(fs)
1447
1448        img = vtki.vtkImageData()
1449        # img.SetOrigin(*pos,1)
1450        tr.RenderString(tp, txt, img, [width, height], dpi)
1451        # RenderString (vtkTextProperty *tprop, const vtkStdString &str,
1452        #   vtkImageData *data, int textDims[2], int dpi, int backend=Default)
1453
1454        blf = vtki.new("ImageBlend")
1455        blf.AddInputData(self.dataset)
1456        blf.AddInputData(img)
1457        blf.SetOpacity(0, 1)
1458        blf.SetOpacity(1, alpha)
1459        blf.SetBlendModeToNormal()
1460        blf.Update()
1461
1462        self._update(blf.GetOutput())
1463        self.pipeline = utils.OperationNode(
1464            "add_text", comment=f"{txt}", parents=[self], c="#f28482"
1465        )
1466        return self
1467
1468    def modified(self) -> Self:
1469        """Use this method in conjunction with `tonumpy()`
1470        to update any modifications to the image array."""
1471        self.dataset.GetPointData().GetScalars().Modified()
1472        return self
1473
1474    def write(self, filename: str) -> Self:
1475        """Write image to file as png or jpg."""
1476        vedo.file_io.write(self, filename)
1477        self.pipeline = utils.OperationNode(
1478            "write",
1479            comment=filename[:15],
1480            parents=[self],
1481            c="#8a817c",
1482            shape="cylinder",
1483        )
1484        return self

Class used to represent 2D images in a 3D world.

Image(obj=None, channels=3)
150    def __init__(self, obj=None, channels=3):
151        """
152        Can be instantiated with a path file name or with a numpy array.
153        Can also be instantiated with a matplotlib figure.
154
155        By default the transparency channel is disabled.
156        To enable it set `channels=4`.
157
158        Use `Image.shape` to get the number of pixels in x and y.
159
160        Arguments:
161            channels :  (int, list)
162                only select these specific rgba channels (useful to remove alpha)
163        """
164        self.name = "Image"
165        self.filename = ""
166        self.file_size = 0
167        self.pipeline = None
168        self.time = 0
169        self.rendered_at = set()
170        self.info = {}
171
172        self.actor = vtki.vtkImageActor()
173        self.actor.retrieve_object = weak_ref_to(self)
174        self.properties = self.actor.GetProperty()
175
176        self.transform = vedo.LinearTransform()
177
178        if utils.is_sequence(obj) and len(obj) > 0:  # passing array
179            img = _get_img(obj, False)
180
181        elif isinstance(obj, vtki.vtkImageData):
182            img = obj
183
184        elif isinstance(obj, str):
185            img = _get_img(obj)
186            self.filename = obj
187
188        elif "matplotlib" in str(obj.__class__):
189            fig = obj
190            if hasattr(fig, "gcf"):
191                fig = fig.gcf()
192            fig.tight_layout(pad=1)
193            fig.canvas.draw()
194
195            # self.array = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8)
196            # self.array = self.array.reshape(fig.canvas.get_width_height()[::-1] + (3,))
197            width, height = fig.get_size_inches() * fig.get_dpi()
198            self.array = np.frombuffer(
199                fig.canvas.buffer_rgba(), dtype=np.uint8
200            ).reshape((int(height), int(width), 4))
201            self.array = self.array[:, :, :3]
202
203            img = _get_img(self.array)
204
205        else:
206            img = vtki.vtkImageData()
207
208        ############# select channels
209        if isinstance(channels, int):
210            channels = list(range(channels))
211
212        nchans = len(channels)
213        n = img.GetPointData().GetScalars().GetNumberOfComponents()
214        if nchans and n > nchans:
215            pec = vtki.new("ImageExtractComponents")
216            pec.SetInputData(img)
217            if nchans == 4:
218                pec.SetComponents(channels[0], channels[1], channels[2], channels[3])
219            elif nchans == 3:
220                pec.SetComponents(channels[0], channels[1], channels[2])
221            elif nchans == 2:
222                pec.SetComponents(channels[0], channels[1])
223            elif nchans == 1:
224                pec.SetComponents(channels[0])
225            pec.Update()
226            img = pec.GetOutput()
227
228        self.dataset = img
229        self.actor.SetInputData(img)
230        self.mapper = self.actor.GetMapper()
231
232        sx, sy, _ = self.dataset.GetDimensions()
233        shape = np.array([sx, sy])
234        self.pipeline = utils.OperationNode("Image", comment=f"#shape {shape}", c="#f28482")

Can be instantiated with a path file name or with a numpy array. Can also be instantiated with a matplotlib figure.

By default the transparency channel is disabled. To enable it set channels=4.

Use Image.shape to get the number of pixels in x and y.

Arguments:
  • channels : (int, list) only select these specific rgba channels (useful to remove alpha)
def dimensions(self) -> numpy.ndarray:
365    def dimensions(self) -> np.ndarray:
366        """
367        Return the image dimension as number of pixels in x and y. 
368        Alias of property `shape`.
369        """
370        nx, ny, _ = self.dataset.GetDimensions()
371        return np.array([nx, ny])

Return the image dimension as number of pixels in x and y. Alias of property shape.

shape: numpy.ndarray
373    @property
374    def shape(self) -> np.ndarray:
375        """Return the image shape as number of pixels in x and y"""
376        return np.array(self.dimensions())

Return the image shape as number of pixels in x and y

channels: int
378    @property
379    def channels(self) -> int:
380        """Return the number of channels in image"""
381        return self.dataset.GetPointData().GetScalars().GetNumberOfComponents()

Return the number of channels in image

extent: Tuple[int, int, int, int]
383    @property
384    def extent(self) -> Tuple[int, int, int, int]:
385        """Return the physical extent that the image spans."""
386        return self.dataset.GetExtent()

Return the physical extent that the image spans.

def copy(self) -> Image:
394    def copy(self) -> "Image":
395        """Return a copy of the image. Alias of `clone()`."""
396        return self.clone()

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

def clone(self) -> Image:
398    def clone(self) -> "Image":
399        """Return an exact copy of the input Image.
400        If transform is True, it is given the same scaling and position."""
401        img = vtki.vtkImageData()
402        img.DeepCopy(self.dataset)
403        pic = Image(img)
404        pic.name = self.name
405        pic.filename = self.filename
406        pic.apply_transform(self.transform)
407        pic.properties = vtki.vtkImageProperty()
408        pic.properties.DeepCopy(self.properties)
409        pic.actor.SetProperty(pic.properties)
410        pic.pipeline = utils.OperationNode("clone", parents=[self], c="#f7dada", shape="diamond")
411        return pic

Return an exact copy of the input Image. If transform is True, it is given the same scaling and position.

def clone2d(self, pos=(0, 0), size=1, justify='') -> vedo.visual.Actor2D:
413    def clone2d(self, pos=(0, 0), size=1, justify="") -> "vedo.visual.Actor2D":
414        """
415        Embed an image as a static 2D image in the canvas.
416        
417        Return a 2D (an `Actor2D`) copy of the input Image.
418        
419        Arguments:
420            pos : (list, str)
421                2D (x,y) position in range [0,1],
422                [0,0] being the bottom-left corner  
423            size : (float)
424                apply a scaling factor to the image
425            justify : (str)
426                define the anchor point ("top-left", "top-center", ...)
427        """
428        pic = vedo.visual.Actor2D()
429
430        pic.name = self.name
431        pic.filename = self.filename
432        pic.file_size = self.file_size
433        
434        pic.dataset = self.dataset
435
436        pic.properties = pic.GetProperty()
437        pic.properties.SetDisplayLocationToBackground()
438
439        if size != 1:
440            newsize = np.array(self.dataset.GetDimensions()[:2]) * size
441            newsize = newsize.astype(int)
442            rsz = vtki.new("ImageResize")
443            rsz.SetInputData(self.dataset)
444            rsz.SetResizeMethodToOutputDimensions()
445            rsz.SetOutputDimensions(newsize[0], newsize[1], 1)
446            rsz.Update()
447            pic.dataset = rsz.GetOutput()
448
449        if justify:
450            pic.dataset, pos = _set_justification(pic.dataset, justify)
451        else:
452            pic.dataset, pos = _set_justification(pic.dataset, pos)
453
454        pic.mapper = vtki.new("ImageMapper")
455        # pic.SetMapper(pic.mapper)
456        pic.mapper.SetInputData(pic.dataset)
457        pic.mapper.SetColorWindow(255)
458        pic.mapper.SetColorLevel(127.5)
459
460        pic.GetPositionCoordinate().SetCoordinateSystem(3)
461        pic.SetPosition(pos)
462
463        pic.pipeline = utils.OperationNode("clone2d", parents=[self], c="#f7dada", shape="diamond")
464        return pic

Embed an image as a static 2D image in the canvas.

Return a 2D (an Actor2D) copy of the input Image.

Arguments:
  • pos : (list, str) 2D (x,y) position in range [0,1], [0,0] being the bottom-left corner
  • size : (float) apply a scaling factor to the image
  • justify : (str) define the anchor point ("top-left", "top-center", ...)
def crop(self, top=None, bottom=None, right=None, left=None, pixels=False) -> Self:
467    def crop(self, top=None, bottom=None, right=None, left=None, pixels=False) -> Self:
468        """
469        Crop image.
470
471        Arguments:
472            top : (float)
473                fraction to crop from the top margin
474            bottom : (float)
475                fraction to crop from the bottom margin
476            left : (float)
477                fraction to crop from the left margin
478            right : (float)
479                fraction to crop from the right margin
480            pixels : (bool)
481                units are pixels
482        """
483        extractVOI = vtki.new("ExtractVOI")
484        extractVOI.SetInputData(self.dataset)
485        extractVOI.IncludeBoundaryOn()
486
487        d = self.dataset.GetDimensions()
488        if pixels:
489            extractVOI.SetVOI(left, d[0] - right - 1, bottom, d[1] - top - 1, 0, 0)
490        else:
491            bx0, bx1, by0, by1 = 0, d[0]-1, 0, d[1]-1
492            if left is not None:   bx0 = int((d[0]-1)*left)
493            if right is not None:  bx1 = int((d[0]-1)*(1-right))
494            if bottom is not None: by0 = int((d[1]-1)*bottom)
495            if top is not None:    by1 = int((d[1]-1)*(1-top))
496            extractVOI.SetVOI(bx0, bx1, by0, by1, 0, 0)
497        extractVOI.Update()
498
499        self._update(extractVOI.GetOutput())
500        self.pipeline = utils.OperationNode(
501            "crop", comment=f"shape={tuple(self.shape)}", parents=[self], c="#f28482"
502        )
503        return self

Crop image.

Arguments:
  • top : (float) fraction to crop from the top margin
  • bottom : (float) fraction to crop from the bottom margin
  • left : (float) fraction to crop from the left margin
  • right : (float) fraction to crop from the right margin
  • pixels : (bool) units are pixels
def pad(self, pixels=10, value=255) -> Self:
505    def pad(self, pixels=10, value=255) -> Self:
506        """
507        Add the specified number of pixels at the image borders.
508        Pixels can be a list formatted as `[left, right, bottom, top]`.
509
510        Arguments:
511            pixels : (int, list)
512                number of pixels to be added (or a list of length 4)
513            value : (int)
514                intensity value (gray-scale color) of the padding
515        """
516        x0, x1, y0, y1, _z0, _z1 = self.dataset.GetExtent()
517        pf = vtki.new("ImageConstantPad")
518        pf.SetInputData(self.dataset)
519        pf.SetConstant(value)
520        if utils.is_sequence(pixels):
521            pf.SetOutputWholeExtent(
522                x0 - pixels[0], x1 + pixels[1],
523                y0 - pixels[2], y1 + pixels[3],
524                0, 0
525            )
526        else:
527            pf.SetOutputWholeExtent(
528                x0 - pixels, x1 + pixels,
529                y0 - pixels, y1 + pixels,
530                0, 0
531            )
532        pf.Update()
533        self._update(pf.GetOutput())
534        self.pipeline = utils.OperationNode(
535            "pad", comment=f"{pixels} pixels", parents=[self], c="#f28482"
536        )
537        return self

Add the specified number of pixels at the image borders. Pixels can be a list formatted as [left, right, bottom, top].

Arguments:
  • pixels : (int, list) number of pixels to be added (or a list of length 4)
  • value : (int) intensity value (gray-scale color) of the padding
def tile(self, nx=4, ny=4, shift=(0, 0)) -> Image:
539    def tile(self, nx=4, ny=4, shift=(0, 0)) -> "Image":
540        """
541        Generate a tiling from the current image by mirroring and repeating it.
542
543        Arguments:
544            nx : (float)
545                number of repeats along x
546            ny : (float)
547                number of repeats along x
548            shift : (list)
549                shift in x and y in pixels
550        """
551        x0, x1, y0, y1, z0, z1 = self.dataset.GetExtent()
552        constant_pad = vtki.new("ImageMirrorPad")
553        constant_pad.SetInputData(self.dataset)
554        constant_pad.SetOutputWholeExtent(
555            int(x0 + shift[0] + 0.5),
556            int(x1 * nx + shift[0] + 0.5),
557            int(y0 + shift[1] + 0.5),
558            int(y1 * ny + shift[1] + 0.5),
559            z0,
560            z1,
561        )
562        constant_pad.Update()
563        img = Image(constant_pad.GetOutput())
564
565        img.pipeline = utils.OperationNode(
566            "tile", comment=f"by {nx}x{ny}", parents=[self], c="#f28482"
567        )
568        return img

Generate a tiling from the current image by mirroring and repeating it.

Arguments:
  • nx : (float) number of repeats along x
  • ny : (float) number of repeats along x
  • shift : (list) shift in x and y in pixels
def append(self, images: list, axis='z', preserve_extents=False) -> Self:
570    def append(self, images: list, axis="z", preserve_extents=False) -> Self:
571        """
572        Append the input images to the current one along the specified axis.
573        Except for the append axis, all inputs must have the same extent.
574        All inputs must have the same number of scalar components.
575        The output has the same origin and spacing as the first input.
576        The origin and spacing of all other inputs are ignored.
577        All inputs must have the same scalar type.
578
579        Arguments:
580            axis : (int, str)
581                axis expanded to hold the multiple images
582            preserve_extents : (bool)
583                if True, the extent of the inputs is used to place
584                the image in the output. The whole extent of the output is the union of the input
585                whole extents. Any portion of the output not covered by the inputs is set to zero.
586                The origin and spacing is taken from the first input.
587
588        Example:
589            ```python
590            from vedo import Image, dataurl
591            pic = Image(dataurl+'dog.jpg').pad()
592            pic.append([pic, pic], axis='y')
593            pic.append([pic, pic, pic], axis='x')
594            pic.show(axes=1).close()
595            ```
596            ![](https://vedo.embl.es/images/feats/pict_append.png)
597        """
598        ima = vtki.new("ImageAppend")
599        ima.SetInputData(self.dataset)
600        for p in images:
601            if isinstance(p, vtki.vtkImageData):
602                ima.AddInputData(p)
603            else:
604                ima.AddInputData(p.dataset)
605        ima.SetPreserveExtents(preserve_extents)
606        if axis == "x":
607            axis = 0
608        elif axis == "y":
609            axis = 1
610        ima.SetAppendAxis(axis)
611        ima.Update()
612        self._update(ima.GetOutput())
613        self.pipeline = utils.OperationNode(
614            "append", comment=f"axis={axis}", parents=[self, *images], c="#f28482"
615        )
616        return self

Append the input images to the current one along the specified axis. 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 Image, dataurl
pic = Image(dataurl+'dog.jpg').pad()
pic.append([pic, pic], axis='y')
pic.append([pic, pic, pic], axis='x')
pic.show(axes=1).close()

def resize(self, newsize: Any) -> Self:
618    def resize(self, newsize: Any) -> Self:
619        """
620        Resize the image resolution by specifying the number of pixels in width and height.
621        If left to zero, it will be automatically calculated to keep the original aspect ratio.
622
623        `newsize` is the shape of image as [npx, npy], or it can be also expressed as a fraction.
624        """
625        old_dims = np.array(self.dataset.GetDimensions())
626
627        if not utils.is_sequence(newsize):
628            newsize = (old_dims * newsize + 0.5).astype(int)
629
630        if not newsize[1]:
631            ar = old_dims[1] / old_dims[0]
632            newsize = [newsize[0], int(newsize[0] * ar + 0.5)]
633        if not newsize[0]:
634            ar = old_dims[0] / old_dims[1]
635            newsize = [int(newsize[1] * ar + 0.5), newsize[1]]
636        newsize = [newsize[0], newsize[1], old_dims[2]]
637
638        rsz = vtki.new("ImageResize")
639        rsz.SetInputData(self.dataset)
640        rsz.SetResizeMethodToOutputDimensions()
641        rsz.SetOutputDimensions(newsize)
642        rsz.Update()
643        out = rsz.GetOutput()
644        out.SetSpacing(1, 1, 1)
645        self._update(out)
646        self.pipeline = utils.OperationNode(
647            "resize", comment=f"shape={tuple(self.shape)}", parents=[self], c="#f28482"
648        )
649        return self

Resize the image resolution by specifying the number of pixels in width and height. If left to zero, it will be automatically calculated to keep the original aspect ratio.

newsize is the shape of image as [npx, npy], or it can be also expressed as a fraction.

def mirror(self, axis='x') -> Self:
651    def mirror(self, axis="x") -> Self:
652        """Mirror image along x or y axis. Same as `flip()`."""
653        ff = vtki.new("ImageFlip")
654        ff.SetInputData(self.dataset)
655        if axis.lower() == "x":
656            ff.SetFilteredAxis(0)
657        elif axis.lower() == "y":
658            ff.SetFilteredAxis(1)
659        else:
660            colors.printc("Error in mirror(): mirror must be set to x or y.", c="r")
661            raise RuntimeError()
662        ff.Update()
663        self._update(ff.GetOutput())
664        self.pipeline = utils.OperationNode(f"mirror {axis}", parents=[self], c="#f28482")
665        return self

Mirror image along x or y axis. Same as flip().

def flip(self, axis='y') -> Self:
667    def flip(self, axis="y") -> Self:
668        """Mirror image along x or y axis. Same as `mirror()`."""
669        return self.mirror(axis=axis)

Mirror image along x or y axis. Same as mirror().

def select(self, component: int) -> Image:
671    def select(self, component: int) -> "Image":
672        """Select one single component of the rgb image."""
673        ec = vtki.new("ImageExtractComponents")
674        ec.SetInputData(self.dataset)
675        ec.SetComponents(component)
676        ec.Update()
677        pic = Image(ec.GetOutput())
678        pic.pipeline = utils.OperationNode(
679            "select", comment=f"component {component}", parents=[self], c="#f28482"
680        )
681        return pic

Select one single component of the rgb image.

def bw(self) -> Self:
683    def bw(self) -> Self:
684        """Make it black and white using luminance calibration."""
685        n = self.dataset.GetPointData().GetNumberOfComponents()
686        if n == 4:
687            ecr = vtki.new("ImageExtractComponents")
688            ecr.SetInputData(self.dataset)
689            ecr.SetComponents(0, 1, 2)
690            ecr.Update()
691            img = ecr.GetOutput()
692        else:
693            img = self.dataset
694
695        ecr = vtki.new("ImageLuminance")
696        ecr.SetInputData(img)
697        ecr.Update()
698        self._update(ecr.GetOutput())
699        self.pipeline = utils.OperationNode("black&white", parents=[self], c="#f28482")
700        return self

Make it black and white using luminance calibration.

def smooth(self, sigma=3, radius=None) -> Self:
702    def smooth(self, sigma=3, radius=None) -> Self:
703        """
704        Smooth a `Image` with Gaussian kernel.
705
706        Arguments:
707            sigma : (int)
708                number of sigmas in pixel units
709            radius : (float)
710                how far out the gaussian kernel will go before being clamped to zero
711        """
712        gsf = vtki.new("ImageGaussianSmooth")
713        gsf.SetDimensionality(2)
714        gsf.SetInputData(self.dataset)
715        if radius is not None:
716            if utils.is_sequence(radius):
717                gsf.SetRadiusFactors(radius[0], radius[1])
718            else:
719                gsf.SetRadiusFactor(radius)
720
721        if utils.is_sequence(sigma):
722            gsf.SetStandardDeviations(sigma[0], sigma[1])
723        else:
724            gsf.SetStandardDeviation(sigma)
725        gsf.Update()
726        self._update(gsf.GetOutput())
727        self.pipeline = utils.OperationNode(
728            "smooth", comment=f"sigma={sigma}", parents=[self], c="#f28482"
729        )
730        return self

Smooth a Image with Gaussian kernel.

Arguments:
  • sigma : (int) number of sigmas in pixel units
  • radius : (float) how far out the gaussian kernel will go before being clamped to zero
def median(self) -> Self:
732    def median(self) -> Self:
733        """
734        Median filter that preserves thin lines and corners.
735
736        It operates on a 5x5 pixel neighborhood. It computes two values initially:
737        the median of the + neighbors and the median of the x neighbors.
738        It then computes the median of these two values plus the center pixel.
739        This result of this second median is the output pixel value.
740        """
741        medf = vtki.new("ImageHybridMedian2D")
742        medf.SetInputData(self.dataset)
743        medf.Update()
744        self._update(medf.GetOutput())
745        self.pipeline = utils.OperationNode("median", parents=[self], c="#f28482")
746        return self

Median filter that preserves thin lines and corners.

It operates on a 5x5 pixel neighborhood. It computes two values initially: the median of the + neighbors and the median of the x neighbors. It then computes the median of these two values plus the center pixel. This result of this second median is the output pixel value.

def enhance(self) -> Self:
748    def enhance(self) -> Self:
749        """
750        Enhance a b&w image using the laplacian, enhancing high-freq edges.
751
752        Example:
753            ```python
754            from vedo import *
755            pic = Image(dataurl+'images/dog.jpg').bw()
756            show(pic, pic.clone().enhance(), N=2, mode='image', zoom='tight')
757            ```
758            ![](https://vedo.embl.es/images/feats/pict_enhance.png)
759        """
760        img = self.dataset
761        scalarRange = img.GetPointData().GetScalars().GetRange()
762
763        cast = vtki.new("ImageCast")
764        cast.SetInputData(img)
765        cast.SetOutputScalarTypeToDouble()
766        cast.Update()
767
768        laplacian = vtki.new("ImageLaplacian")
769        laplacian.SetInputData(cast.GetOutput())
770        laplacian.SetDimensionality(2)
771        laplacian.Update()
772
773        subtr = vtki.new("ImageMathematics")
774        subtr.SetInputData(0, cast.GetOutput())
775        subtr.SetInputData(1, laplacian.GetOutput())
776        subtr.SetOperationToSubtract()
777        subtr.Update()
778
779        color_window = scalarRange[1] - scalarRange[0]
780        color_level = color_window / 2
781        original_color = vtki.new("ImageMapToWindowLevelColors")
782        original_color.SetWindow(color_window)
783        original_color.SetLevel(color_level)
784        original_color.SetInputData(subtr.GetOutput())
785        original_color.Update()
786        self._update(original_color.GetOutput())
787
788        self.pipeline = utils.OperationNode("enhance", parents=[self], c="#f28482")
789        return self

Enhance a b&w image using the laplacian, enhancing high-freq edges.

Example:
from vedo import *
pic = Image(dataurl+'images/dog.jpg').bw()
show(pic, pic.clone().enhance(), N=2, mode='image', zoom='tight')

def fft(self, mode='magnitude', logscale=12, center=True) -> Image:
791    def fft(self, mode="magnitude", logscale=12, center=True) -> "Image":
792        """
793        Fast Fourier transform of a image.
794
795        Arguments:
796            logscale : (float)
797                if non-zero, take the logarithm of the intensity and scale it by this factor.
798            mode : (str)
799                either [magnitude, real, imaginary, complex], compute the point array data accordingly.
800            center : (bool)
801                shift constant zero-frequency to the center of the image for display.
802                (FFT converts spatial images into frequency space, but puts the zero frequency at the origin)
803        """
804        ffti = vtki.new("ImageFFT")
805        ffti.SetInputData(self.dataset)
806        ffti.Update()
807
808        if "mag" in mode:
809            mag = vtki.new("ImageMagnitude")
810            mag.SetInputData(ffti.GetOutput())
811            mag.Update()
812            out = mag.GetOutput()
813        elif "real" in mode:
814            erf = vtki.new("ImageExtractComponents")
815            erf.SetInputData(ffti.GetOutput())
816            erf.SetComponents(0)
817            erf.Update()
818            out = erf.GetOutput()
819        elif "imaginary" in mode:
820            eimf = vtki.new("ImageExtractComponents")
821            eimf.SetInputData(ffti.GetOutput())
822            eimf.SetComponents(1)
823            eimf.Update()
824            out = eimf.GetOutput()
825        elif "complex" in mode:
826            out = ffti.GetOutput()
827        else:
828            colors.printc("Error in fft(): unknown mode", mode)
829            raise RuntimeError()
830
831        if center:
832            center = vtki.new("ImageFourierCenter")
833            center.SetInputData(out)
834            center.Update()
835            out = center.GetOutput()
836
837        if "complex" not in mode:
838            if logscale:
839                ils = vtki.new("ImageLogarithmicScale")
840                ils.SetInputData(out)
841                ils.SetConstant(logscale)
842                ils.Update()
843                out = ils.GetOutput()
844
845        pic = Image(out)
846        pic.pipeline = utils.OperationNode("FFT", parents=[self], c="#f28482")
847        return pic

Fast Fourier transform of a image.

Arguments:
  • logscale : (float) if non-zero, take the logarithm of the intensity and scale it by this factor.
  • mode : (str) either [magnitude, real, imaginary, complex], compute the point array data accordingly.
  • center : (bool) shift constant zero-frequency to the center of the image for display. (FFT converts spatial images into frequency space, but puts the zero frequency at the origin)
def rfft(self, mode='magnitude') -> Image:
849    def rfft(self, mode="magnitude") -> "Image":
850        """Reverse Fast Fourier transform of a image."""
851
852        ffti = vtki.new("ImageRFFT")
853        ffti.SetInputData(self.dataset)
854        ffti.Update()
855
856        if "mag" in mode:
857            mag = vtki.new("ImageMagnitude")
858            mag.SetInputData(ffti.GetOutput())
859            mag.Update()
860            out = mag.GetOutput()
861        elif "real" in mode:
862            erf = vtki.new("ImageExtractComponents")
863            erf.SetInputData(ffti.GetOutput())
864            erf.SetComponents(0)
865            erf.Update()
866            out = erf.GetOutput()
867        elif "imaginary" in mode:
868            eimf = vtki.new("ImageExtractComponents")
869            eimf.SetInputData(ffti.GetOutput())
870            eimf.SetComponents(1)
871            eimf.Update()
872            out = eimf.GetOutput()
873        elif "complex" in mode:
874            out = ffti.GetOutput()
875        else:
876            colors.printc("Error in rfft(): unknown mode", mode)
877            raise RuntimeError()
878
879        pic = Image(out)
880        pic.pipeline = utils.OperationNode("rFFT", parents=[self], c="#f28482")
881        return pic

Reverse Fast Fourier transform of a image.

def filterpass(self, lowcutoff=None, highcutoff=None, order=3) -> Self:
883    def filterpass(self, lowcutoff=None, highcutoff=None, order=3) -> Self:
884        """
885        Low-pass and high-pass filtering become trivial in the frequency domain.
886        A portion of the pixels/voxels are simply masked or attenuated.
887        This function applies a high pass Butterworth filter that attenuates the
888        frequency domain image with the function
889
890        The gradual attenuation of the filter is important.
891        A simple high-pass filter would simply mask a set of pixels in the frequency domain,
892        but the abrupt transition would cause a ringing effect in the spatial domain.
893
894        Arguments:
895            lowcutoff : (list)
896                the cutoff frequencies
897            highcutoff : (list)
898                the cutoff frequencies
899            order : (int)
900                order determines sharpness of the cutoff curve
901        """
902        # https://lorensen.github.io/VTKExamples/site/Cxx/ImageProcessing/IdealHighPass
903        fft = vtki.new("ImageFFT")
904        fft.SetInputData(self.dataset)
905        fft.Update()
906        out = fft.GetOutput()
907
908        if highcutoff:
909            blp = vtki.new("ImageButterworthLowPass")
910            blp.SetInputData(out)
911            blp.SetCutOff(highcutoff)
912            blp.SetOrder(order)
913            blp.Update()
914            out = blp.GetOutput()
915
916        if lowcutoff:
917            bhp = vtki.new("ImageButterworthHighPass")
918            bhp.SetInputData(out)
919            bhp.SetCutOff(lowcutoff)
920            bhp.SetOrder(order)
921            bhp.Update()
922            out = bhp.GetOutput()
923
924        rfft = vtki.new("ImageRFFT")
925        rfft.SetInputData(out)
926        rfft.Update()
927
928        ecomp = vtki.new("ImageExtractComponents")
929        ecomp.SetInputData(rfft.GetOutput())
930        ecomp.SetComponents(0)
931        ecomp.Update()
932
933        caster = vtki.new("ImageCast")
934        caster.SetOutputScalarTypeToUnsignedChar()
935        caster.SetInputData(ecomp.GetOutput())
936        caster.Update()
937        self._update(caster.GetOutput())
938        self.pipeline = utils.OperationNode("filterpass", parents=[self], c="#f28482")
939        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 with the function

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:
  • lowcutoff : (list) the cutoff frequencies
  • highcutoff : (list) the cutoff frequencies
  • order : (int) order determines sharpness of the cutoff curve
def blend(self, pic, alpha1=0.5, alpha2=0.5) -> Self:
941    def blend(self, pic, alpha1=0.5, alpha2=0.5) -> Self:
942        """
943        Take L, LA, RGB, or RGBA images as input and blends
944        them according to the alpha values and/or the opacity setting for each input.
945        """
946        blf = vtki.new("ImageBlend")
947        blf.AddInputData(self.dataset)
948        blf.AddInputData(pic.dataset)
949        blf.SetOpacity(0, alpha1)
950        blf.SetOpacity(1, alpha2)
951        blf.SetBlendModeToNormal()
952        blf.Update()
953        self._update(blf.GetOutput())
954        self.pipeline = utils.OperationNode("blend", parents=[self, pic], c="#f28482")
955        return self

Take L, LA, RGB, or RGBA images as input and blends them according to the alpha values and/or the opacity setting for each input.

def warp( self, source_pts=(), target_pts=(), transform=None, sigma=1, mirroring=False, bc='w', alpha=1) -> Self:
 957    def warp(
 958        self,
 959        source_pts=(),
 960        target_pts=(),
 961        transform=None,
 962        sigma=1,
 963        mirroring=False,
 964        bc="w",
 965        alpha=1,
 966    ) -> Self:
 967        """
 968        Warp an image using thin-plate splines.
 969
 970        Arguments:
 971            source_pts : (list)
 972                source points
 973            target_pts : (list)
 974                target points
 975            transform : (vtkTransform)
 976                a vtkTransform object can be supplied
 977            sigma : (float), optional
 978                stiffness of the interpolation
 979            mirroring : (bool)
 980                fill the margins with a reflection of the original image
 981            bc : (color)
 982                fill the margins with a solid color
 983            alpha : (float)
 984                opacity of the filled margins
 985        """
 986        if transform is None:
 987            # source and target must be filled
 988            transform = vtki.vtkThinPlateSplineTransform()
 989            transform.SetBasisToR2LogR()
 990
 991            parents = [self]
 992            if isinstance(source_pts, vedo.Points):
 993                parents.append(source_pts)
 994                source_pts = source_pts.vertices
 995            if isinstance(target_pts, vedo.Points):
 996                parents.append(target_pts)
 997                target_pts = target_pts.vertices
 998
 999            ns = len(source_pts)
1000            nt = len(target_pts)
1001            if ns != nt:
1002                colors.printc("Error in image.warp(): #source != #target points", ns, nt, c="r")
1003                raise RuntimeError()
1004
1005            ptsou = vtki.vtkPoints()
1006            ptsou.SetNumberOfPoints(ns)
1007
1008            pttar = vtki.vtkPoints()
1009            pttar.SetNumberOfPoints(nt)
1010
1011            for i in range(ns):
1012                p = source_pts[i]
1013                ptsou.SetPoint(i, [p[0], p[1], 0])
1014                p = target_pts[i]
1015                pttar.SetPoint(i, [p[0], p[1], 0])
1016
1017            transform.SetSigma(sigma)
1018            transform.SetSourceLandmarks(pttar)
1019            transform.SetTargetLandmarks(ptsou)
1020        else:
1021            # ignore source and target
1022            pass
1023
1024        reslice = vtki.new("ImageReslice")
1025        reslice.SetInputData(self.dataset)
1026        reslice.SetOutputDimensionality(2)
1027        reslice.SetResliceTransform(transform)
1028        reslice.SetInterpolationModeToCubic()
1029        reslice.SetMirror(mirroring)
1030        c = np.array(colors.get_color(bc)) * 255
1031        reslice.SetBackgroundColor([c[0], c[1], c[2], alpha * 255])
1032        reslice.Update()
1033        self._update(reslice.GetOutput())
1034        self.pipeline = utils.OperationNode("warp", parents=parents, c="#f28482")
1035        return self

Warp an image using thin-plate splines.

Arguments:
  • source_pts : (list) source points
  • target_pts : (list) target points
  • transform : (vtkTransform) a vtkTransform object can be supplied
  • sigma : (float), optional stiffness of the interpolation
  • mirroring : (bool) fill the margins with a reflection of the original image
  • bc : (color) fill the margins with a solid color
  • alpha : (float) opacity of the filled margins
def invert(self) -> Self:
1037    def invert(self) -> Self:
1038        """
1039        Return an inverted image (inverted in each color channel).
1040        """
1041        rgb = self.tonumpy()
1042        data = 255 - np.array(rgb)
1043        self._update(_get_img(data))
1044        self.pipeline = utils.OperationNode("invert", parents=[self], c="#f28482")
1045        return self

Return an inverted image (inverted in each color channel).

def binarize(self, threshold=None, invert=False) -> Self:
1047    def binarize(self, threshold=None, invert=False) -> Self:
1048        """
1049        Return a new Image where pixel above threshold are set to 255
1050        and pixels below are set to 0.
1051
1052        Arguments:
1053            threshold : (float)
1054                input threshold value
1055            invert : (bool)
1056                invert threshold direction
1057
1058        Example:
1059        ```python
1060        from vedo import Image, show
1061        pic1 = Image("https://aws.glamour.es/prod/designs/v1/assets/620x459/547577.jpg")
1062        pic2 = pic1.clone().invert()
1063        pic3 = pic1.clone().binarize()
1064        show(pic1, pic2, pic3, N=3, bg="blue9").close()
1065        ```
1066        ![](https://vedo.embl.es/images/feats/pict_binarize.png)
1067        """
1068        rgb = self.tonumpy()
1069        if rgb.ndim == 3:
1070            intensity = np.sum(rgb, axis=2) / 3
1071        else:
1072            intensity = rgb
1073
1074        if threshold is None:
1075            vmin, vmax = np.min(intensity), np.max(intensity)
1076            threshold = (vmax + vmin) / 2
1077
1078        data = np.zeros_like(intensity).astype(np.uint8)
1079        mask = np.where(intensity > threshold)
1080        if invert:
1081            data += 255
1082            data[mask] = 0
1083        else:
1084            data[mask] = 255
1085
1086        self._update(_get_img(data, flip=True))
1087
1088        self.pipeline = utils.OperationNode(
1089            "binarize", comment=f"threshold={threshold}", parents=[self], c="#f28482"
1090        )
1091        return self

Return a new Image where pixel above threshold are set to 255 and pixels below are set to 0.

Arguments:
  • threshold : (float) input threshold value
  • invert : (bool) invert threshold direction

Example:

from vedo import Image, show
pic1 = Image("https://aws.glamour.es/prod/designs/v1/assets/620x459/547577.jpg")
pic2 = pic1.clone().invert()
pic3 = pic1.clone().binarize()
show(pic1, pic2, pic3, N=3, bg="blue9").close()

def threshold(self, value=None, flip=False) -> vedo.mesh.Mesh:
1093    def threshold(self, value=None, flip=False) -> "vedo.Mesh":
1094        """
1095        Create a polygonal Mesh from a Image by filling regions with pixels
1096        luminosity above a specified value.
1097
1098        Arguments:
1099            value : (float)
1100                The default is None, e.i. 1/3 of the scalar range.
1101            flip: (bool)
1102                Flip polygon orientations
1103
1104        Returns:
1105            A polygonal mesh.
1106        """
1107        mgf = vtki.new("ImageMagnitude")
1108        mgf.SetInputData(self.dataset)
1109        mgf.Update()
1110        msq = vtki.new("MarchingSquares")
1111        msq.SetInputData(mgf.GetOutput())
1112        if value is None:
1113            r0, r1 = self.dataset.GetScalarRange()
1114            value = r0 + (r1 - r0) / 3
1115        msq.SetValue(0, value)
1116        msq.Update()
1117        if flip:
1118            rs = vtki.new("ReverseSense")
1119            rs.SetInputData(msq.GetOutput())
1120            rs.ReverseCellsOn()
1121            rs.ReverseNormalsOff()
1122            rs.Update()
1123            output = rs.GetOutput()
1124        else:
1125            output = msq.GetOutput()
1126        ctr = vtki.new("ContourTriangulator")
1127        ctr.SetInputData(output)
1128        ctr.Update()
1129        out = vedo.Mesh(ctr.GetOutput(), c="k").bc("t").lighting("off")
1130
1131        out.pipeline = utils.OperationNode(
1132            "threshold", comment=f"{value: .2f}", parents=[self], c="#f28482:#e9c46a"
1133        )
1134        return out

Create a polygonal Mesh from a Image by filling regions with pixels luminosity above a specified value.

Arguments:
  • value : (float) The default is None, e.i. 1/3 of the scalar range.
  • flip: (bool) Flip polygon orientations
Returns:

A polygonal mesh.

def cmap(self, name: str, vmin=None, vmax=None) -> Self:
1136    def cmap(self, name: str, vmin=None, vmax=None) -> Self:
1137        """Colorize a image with a colormap representing pixel intensity"""
1138        n = self.dataset.GetPointData().GetNumberOfComponents()
1139        if n > 1:
1140            ecr = vtki.new("ImageExtractComponents")
1141            ecr.SetInputData(self.dataset)
1142            ecr.SetComponents(0, 1, 2)
1143            ecr.Update()
1144            ilum = vtki.new("ImageMagnitude")
1145            ilum.SetInputData(self.dataset)
1146            ilum.Update()
1147            img = ilum.GetOutput()
1148        else:
1149            img = self.dataset
1150
1151        lut = vtki.vtkLookupTable()
1152        _vmin, _vmax = img.GetScalarRange()
1153        if vmin is not None:
1154            _vmin = vmin
1155        if vmax is not None:
1156            _vmax = vmax
1157        lut.SetRange(_vmin, _vmax)
1158
1159        ncols = 256
1160        lut.SetNumberOfTableValues(ncols)
1161        cols = colors.color_map(range(ncols), name, 0, ncols)
1162        for i, c in enumerate(cols):
1163            lut.SetTableValue(i, *c)
1164        lut.Build()
1165
1166        imap = vtki.new("ImageMapToColors")
1167        imap.SetLookupTable(lut)
1168        imap.SetInputData(img)
1169        imap.Update()
1170        self._update(imap.GetOutput())
1171        self.pipeline = utils.OperationNode(
1172            "cmap", comment=f'"{name}"', parents=[self], c="#f28482"
1173        )
1174        return self

Colorize a image with a colormap representing pixel intensity

def rotate( self, angle: float, center=(), scale=1.0, mirroring=False, bc='w', alpha=1.0) -> Self:
1176    def rotate(self, angle: float, center=(), scale=1.0, mirroring=False, bc="w", alpha=1.0) -> Self:
1177        """
1178        Rotate by the specified angle (anticlockwise).
1179
1180        Arguments:
1181            angle : (float)
1182                rotation angle in degrees
1183            center : (list)
1184                center of rotation (x,y) in pixels
1185        """
1186        bounds = self.bounds()
1187        pc = [0, 0, 0]
1188        if center:
1189            pc[0] = center[0]
1190            pc[1] = center[1]
1191        else:
1192            pc[0] = (bounds[1] + bounds[0]) / 2.0
1193            pc[1] = (bounds[3] + bounds[2]) / 2.0
1194        pc[2] = (bounds[5] + bounds[4]) / 2.0
1195
1196        transform = vtki.vtkTransform()
1197        transform.Translate(pc)
1198        transform.RotateWXYZ(-angle, 0, 0, 1)
1199        transform.Scale(1 / scale, 1 / scale, 1)
1200        transform.Translate(-pc[0], -pc[1], -pc[2])
1201
1202        reslice = vtki.new("ImageReslice")
1203        reslice.SetMirror(mirroring)
1204        c = np.array(colors.get_color(bc)) * 255
1205        reslice.SetBackgroundColor([c[0], c[1], c[2], alpha * 255])
1206        reslice.SetInputData(self.dataset)
1207        reslice.SetResliceTransform(transform)
1208        reslice.SetOutputDimensionality(2)
1209        reslice.SetInterpolationModeToCubic()
1210        reslice.AutoCropOutputOn()
1211        reslice.Update()
1212        self._update(reslice.GetOutput())
1213
1214        self.pipeline = utils.OperationNode(
1215            "rotate", comment=f"angle={angle}", parents=[self], c="#f28482"
1216        )
1217        return self

Rotate by the specified angle (anticlockwise).

Arguments:
  • angle : (float) rotation angle in degrees
  • center : (list) center of rotation (x,y) in pixels
def tomesh(self) -> vedo.shapes.Grid:
1219    def tomesh(self) -> "vedo.shapes.Grid":
1220        """
1221        Convert an image to polygonal data (quads),
1222        with each polygon vertex assigned a RGBA value.
1223        """
1224        dims = self.dataset.GetDimensions()
1225        gr = vedo.shapes.Grid(s=dims[:2], res=(dims[0] - 1, dims[1] - 1))
1226        gr.pos(int(dims[0] / 2), int(dims[1] / 2)).pickable(True).wireframe(False).lw(0)
1227        self.dataset.GetPointData().GetScalars().SetName("RGBA")
1228        gr.dataset.GetPointData().AddArray(self.dataset.GetPointData().GetScalars())
1229        gr.dataset.GetPointData().SetActiveScalars("RGBA")
1230        gr.mapper.SetArrayName("RGBA")
1231        gr.mapper.SetScalarModeToUsePointData()
1232        gr.mapper.ScalarVisibilityOn()
1233        gr.name = self.name
1234        gr.filename = self.filename
1235        gr.pipeline = utils.OperationNode("tomesh", parents=[self], c="#f28482:#e9c46a")
1236        return gr

Convert an image to polygonal data (quads), with each polygon vertex assigned a RGBA value.

def tonumpy(self) -> numpy.ndarray:
1238    def tonumpy(self) -> np.ndarray:
1239        """
1240        Get read-write access to pixels of a Image object as a numpy array.
1241        Note that the shape is (nrofchannels, nx, ny).
1242
1243        When you set values in the output image, you don't want numpy to reallocate the array
1244        but instead set values in the existing array, so use the [:] operator.
1245        Example: arr[:] = arr - 15
1246
1247        If the array is modified call:
1248        `image.modified()`
1249        when all your modifications are completed.
1250        """
1251        nx, ny, _ = self.dataset.GetDimensions()
1252        nchan = self.dataset.GetPointData().GetScalars().GetNumberOfComponents()
1253        narray = utils.vtk2numpy(self.dataset.GetPointData().GetScalars()).reshape(ny, nx, nchan)
1254        narray = np.flip(narray, axis=0).astype(np.uint8)
1255        return narray.squeeze()

Get read-write access to pixels of a Image object as a numpy array. Note that the shape is (nrofchannels, nx, ny).

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 - 15

If the array is modified call: image.modified() when all your modifications are completed.

def add_rectangle( self, xspan: List[float], yspan: List[float], c='green5', alpha=1.0) -> Self:
1257    def add_rectangle(self, xspan: List[float], yspan: List[float], c="green5", alpha=1.0) -> Self:
1258        """Draw a rectangle box on top of current image. Units are pixels.
1259
1260        Example:
1261            ```python
1262            import vedo
1263            pic = vedo.Image(vedo.dataurl+"images/dog.jpg")
1264            pic.add_rectangle([100,300], [100,200], c='green4', alpha=0.7)
1265            pic.add_line([100,100],[400,500], lw=2, alpha=1)
1266            pic.add_triangle([250,300], [100,300], [200,400], c='blue5')
1267            vedo.show(pic, axes=1).close()
1268            ```
1269            ![](https://vedo.embl.es/images/feats/pict_drawon.png)
1270        """
1271        x1, x2 = xspan
1272        y1, y2 = yspan
1273
1274        r, g, b = vedo.colors.get_color(c)
1275        c = np.array([r, g, b]) * 255
1276        c = c.astype(np.uint8)
1277
1278        alpha = min(alpha, 1)
1279        if alpha <= 0:
1280            return self
1281        alpha2 = alpha
1282        alpha1 = 1 - alpha
1283
1284        nx, ny = self.dimensions()
1285        if x2 > nx:
1286            x2 = nx - 1
1287        if y2 > ny:
1288            y2 = ny - 1
1289
1290        nchan = self.channels
1291        narrayA = self.tonumpy()
1292
1293        canvas_source = vtki.new("ImageCanvasSource2D")
1294        canvas_source.SetExtent(0, nx - 1, 0, ny - 1, 0, 0)
1295        canvas_source.SetScalarTypeToUnsignedChar()
1296        canvas_source.SetNumberOfScalarComponents(nchan)
1297        canvas_source.SetDrawColor(255, 255, 255)
1298        canvas_source.FillBox(x1, x2, y1, y2)
1299        canvas_source.Update()
1300        imagedataset = canvas_source.GetOutput()
1301
1302        vscals = imagedataset.GetPointData().GetScalars()
1303        narrayB = vedo.utils.vtk2numpy(vscals).reshape(ny, nx, nchan)
1304        narrayB = np.flip(narrayB, axis=0)
1305        narrayC = np.where(narrayB < 255, narrayA, alpha1 * narrayA + alpha2 * c)
1306        self._update(_get_img(narrayC))
1307        self.pipeline = utils.OperationNode("rectangle", parents=[self], c="#f28482")
1308        return self

Draw a rectangle box on top of current image. Units are pixels.

Example:
import vedo
pic = vedo.Image(vedo.dataurl+"images/dog.jpg")
pic.add_rectangle([100,300], [100,200], c='green4', alpha=0.7)
pic.add_line([100,100],[400,500], lw=2, alpha=1)
pic.add_triangle([250,300], [100,300], [200,400], c='blue5')
vedo.show(pic, axes=1).close()

def add_line(self, p1: List[float], p2: List[float], lw=2, c='k2', alpha=1.0) -> Self:
1310    def add_line(self, p1: List[float], p2: List[float], lw=2, c="k2", alpha=1.0) -> Self:
1311        """Draw a line on top of current image. Units are pixels."""
1312        x1, x2 = p1
1313        y1, y2 = p2
1314
1315        r, g, b = vedo.colors.get_color(c)
1316        c = np.array([r, g, b]) * 255
1317        c = c.astype(np.uint8)
1318
1319        alpha = min(alpha, 1)
1320        if alpha <= 0:
1321            return self
1322        alpha2 = alpha
1323        alpha1 = 1 - alpha
1324
1325        nx, ny = self.dimensions()
1326        if x2 > nx:
1327            x2 = nx - 1
1328        if y2 > ny:
1329            y2 = ny - 1
1330
1331        nchan = self.channels
1332        narrayA = self.tonumpy()
1333
1334        canvas_source = vtki.new("ImageCanvasSource2D")
1335        canvas_source.SetExtent(0, nx - 1, 0, ny - 1, 0, 0)
1336        canvas_source.SetScalarTypeToUnsignedChar()
1337        canvas_source.SetNumberOfScalarComponents(nchan)
1338        canvas_source.SetDrawColor(255, 255, 255)
1339        canvas_source.FillTube(x1, x2, y1, y2, lw)
1340        canvas_source.Update()
1341        imagedataset = canvas_source.GetOutput()
1342
1343        vscals = imagedataset.GetPointData().GetScalars()
1344        narrayB = vedo.utils.vtk2numpy(vscals).reshape(ny, nx, nchan)
1345        narrayB = np.flip(narrayB, axis=0)
1346        narrayC = np.where(narrayB < 255, narrayA, alpha1 * narrayA + alpha2 * c)
1347        self._update(_get_img(narrayC))
1348        self.pipeline = utils.OperationNode("line", parents=[self], c="#f28482")
1349        return self

Draw a line on top of current image. Units are pixels.

def add_triangle( self, p1: List[float], p2: List[float], p3: List[float], c='red3', alpha=1.0) -> Self:
1351    def add_triangle(self, p1: List[float], p2: List[float], p3: List[float], c="red3", alpha=1.0) -> Self:
1352        """Draw a triangle on top of current image. Units are pixels."""
1353        x1, y1 = p1
1354        x2, y2 = p2
1355        x3, y3 = p3
1356
1357        r, g, b = vedo.colors.get_color(c)
1358        c = np.array([r, g, b]) * 255
1359        c = c.astype(np.uint8)
1360
1361        alpha = min(alpha, 1)
1362        if alpha <= 0:
1363            return self
1364        alpha2 = alpha
1365        alpha1 = 1 - alpha
1366
1367        nx, ny = self.dimensions()
1368        x1 = min(x1, nx)
1369        x2 = min(x2, nx)
1370        x3 = min(x3, nx)
1371
1372        y1 = min(y1, ny)
1373        y2 = min(y2, ny)
1374        y3 = min(y3, ny)
1375
1376        nchan = self.channels
1377        narrayA = self.tonumpy()
1378
1379        canvas_source = vtki.new("ImageCanvasSource2D")
1380        canvas_source.SetExtent(0, nx - 1, 0, ny - 1, 0, 0)
1381        canvas_source.SetScalarTypeToUnsignedChar()
1382        canvas_source.SetNumberOfScalarComponents(nchan)
1383        canvas_source.SetDrawColor(255, 255, 255)
1384        canvas_source.FillTriangle(x1, y1, x2, y2, x3, y3)
1385        canvas_source.Update()
1386        imagedataset = canvas_source.GetOutput()
1387
1388        vscals = imagedataset.GetPointData().GetScalars()
1389        narrayB = vedo.utils.vtk2numpy(vscals).reshape(ny, nx, nchan)
1390        narrayB = np.flip(narrayB, axis=0)
1391        narrayC = np.where(narrayB < 255, narrayA, alpha1 * narrayA + alpha2 * c)
1392        self._update(_get_img(narrayC))
1393        self.pipeline = utils.OperationNode("triangle", parents=[self], c="#f28482")
1394        return self

Draw a triangle on top of current image. Units are pixels.

def add_text( self, txt: str, width=400, height=200, alpha=1, c='black', bg=None, alpha_bg=1, font='Theemim', dpi=200, justify='bottom-left') -> Self:
1396    def add_text(
1397        self,
1398        txt: str,
1399        width=400,
1400        height=200,
1401        alpha=1,
1402        c="black",
1403        bg=None,
1404        alpha_bg=1,
1405        font="Theemim",
1406        dpi=200,
1407        justify="bottom-left",
1408    ) -> Self:
1409        """Add text to an image."""
1410
1411        tp = vtki.vtkTextProperty()
1412        tp.BoldOff()
1413        tp.FrameOff()
1414        tp.SetColor(colors.get_color(c))
1415        tp.SetJustificationToLeft()
1416        if "top" in justify:
1417            tp.SetVerticalJustificationToTop()
1418        if "bottom" in justify:
1419            tp.SetVerticalJustificationToBottom()
1420        if "cent" in justify:
1421            tp.SetVerticalJustificationToCentered()
1422            tp.SetJustificationToCentered()
1423        if "left" in justify:
1424            tp.SetJustificationToLeft()
1425        if "right" in justify:
1426            tp.SetJustificationToRight()
1427
1428        if   font.lower() == "courier": tp.SetFontFamilyToCourier()
1429        elif font.lower() == "times": tp.SetFontFamilyToTimes()
1430        elif font.lower() == "arial": tp.SetFontFamilyToArial()
1431        else:
1432            tp.SetFontFamily(vtki.VTK_FONT_FILE)
1433            tp.SetFontFile(utils.get_font_path(font))
1434
1435        if bg:
1436            bgcol = colors.get_color(bg)
1437            tp.SetBackgroundColor(bgcol)
1438            tp.SetBackgroundOpacity(alpha_bg)
1439            tp.SetFrameColor(bgcol)
1440            tp.FrameOn()
1441
1442        tr = vtki.new("TextRenderer")
1443        # GetConstrainedFontSize (const vtkUnicodeString &str,
1444        # vtkTextProperty(*tprop, int targetWidth, int targetHeight, int dpi)
1445        fs = tr.GetConstrainedFontSize(txt, tp, width, height, dpi)
1446        tp.SetFontSize(fs)
1447
1448        img = vtki.vtkImageData()
1449        # img.SetOrigin(*pos,1)
1450        tr.RenderString(tp, txt, img, [width, height], dpi)
1451        # RenderString (vtkTextProperty *tprop, const vtkStdString &str,
1452        #   vtkImageData *data, int textDims[2], int dpi, int backend=Default)
1453
1454        blf = vtki.new("ImageBlend")
1455        blf.AddInputData(self.dataset)
1456        blf.AddInputData(img)
1457        blf.SetOpacity(0, 1)
1458        blf.SetOpacity(1, alpha)
1459        blf.SetBlendModeToNormal()
1460        blf.Update()
1461
1462        self._update(blf.GetOutput())
1463        self.pipeline = utils.OperationNode(
1464            "add_text", comment=f"{txt}", parents=[self], c="#f28482"
1465        )
1466        return self

Add text to an image.

def modified(self) -> Self:
1468    def modified(self) -> Self:
1469        """Use this method in conjunction with `tonumpy()`
1470        to update any modifications to the image array."""
1471        self.dataset.GetPointData().GetScalars().Modified()
1472        return self

Use this method in conjunction with tonumpy() to update any modifications to the image array.

def write(self, filename: str) -> Self:
1474    def write(self, filename: str) -> Self:
1475        """Write image to file as png or jpg."""
1476        vedo.file_io.write(self, filename)
1477        self.pipeline = utils.OperationNode(
1478            "write",
1479            comment=filename[:15],
1480            parents=[self],
1481            c="#8a817c",
1482            shape="cylinder",
1483        )
1484        return self

Write image to file as png or jpg.

class Picture(Image):
1487class Picture(Image):
1488    def __init__(self, obj=None, channels=3):
1489        """Deprecated. Use `Image` instead."""
1490        vedo.logger.warning("Picture() is deprecated, use Image() instead.")
1491        super().__init__(obj=obj, channels=channels)

Class used to represent 2D images in a 3D world.

Picture(obj=None, channels=3)
1488    def __init__(self, obj=None, channels=3):
1489        """Deprecated. Use `Image` instead."""
1490        vedo.logger.warning("Picture() is deprecated, use Image() instead.")
1491        super().__init__(obj=obj, channels=channels)

Deprecated. Use Image instead.