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

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

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

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

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

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

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

Return the number of channels in image

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

Return the physical extent that the image spans.

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

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

def clone(self) -> Image:
399    def clone(self) -> "Image":
400        """Return an exact copy of the input Image.
401        If transform is True, it is given the same scaling and position."""
402        img = vtki.vtkImageData()
403        img.DeepCopy(self.dataset)
404        pic = Image(img)
405        pic.name = self.name
406        pic.filename = self.filename
407        pic.apply_transform(self.transform)
408        pic.properties = vtki.vtkImageProperty()
409        pic.properties.DeepCopy(self.properties)
410        pic.actor.SetProperty(pic.properties)
411        pic.pipeline = utils.OperationNode("clone", parents=[self], c="#f7dada", shape="diamond")
412        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:
414    def clone2d(self, pos=(0, 0), size=1, justify="") -> "vedo.visual.Actor2D":
415        """
416        Embed an image as a static 2D image in the canvas.
417        
418        Return a 2D (an `Actor2D`) copy of the input Image.
419        
420        Arguments:
421            pos : (list, str)
422                2D (x,y) position in range [0,1],
423                [0,0] being the bottom-left corner  
424            size : (float)
425                apply a scaling factor to the image
426            justify : (str)
427                define the anchor point ("top-left", "top-center", ...)
428        """
429        pic = vedo.visual.Actor2D()
430
431        pic.name = self.name
432        pic.filename = self.filename
433        pic.file_size = self.file_size
434        
435        pic.dataset = self.dataset
436
437        pic.properties = pic.GetProperty()
438        pic.properties.SetDisplayLocationToBackground()
439
440        if size != 1:
441            newsize = np.array(self.dataset.GetDimensions()[:2]) * size
442            newsize = newsize.astype(int)
443            rsz = vtki.new("ImageResize")
444            rsz.SetInputData(self.dataset)
445            rsz.SetResizeMethodToOutputDimensions()
446            rsz.SetOutputDimensions(newsize[0], newsize[1], 1)
447            rsz.Update()
448            pic.dataset = rsz.GetOutput()
449
450        if justify:
451            pic.dataset, pos = _set_justification(pic.dataset, justify)
452        else:
453            pic.dataset, pos = _set_justification(pic.dataset, pos)
454
455        pic.mapper = vtki.new("ImageMapper")
456        # pic.SetMapper(pic.mapper)
457        pic.mapper.SetInputData(pic.dataset)
458        pic.mapper.SetColorWindow(255)
459        pic.mapper.SetColorLevel(127.5)
460
461        pic.GetPositionCoordinate().SetCoordinateSystem(3)
462        pic.SetPosition(pos)
463
464        pic.pipeline = utils.OperationNode("clone2d", parents=[self], c="#f7dada", shape="diamond")
465        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:
468    def crop(self, top=None, bottom=None, right=None, left=None, pixels=False) -> Self:
469        """
470        Crop image.
471
472        Arguments:
473            top : (float)
474                fraction to crop from the top margin
475            bottom : (float)
476                fraction to crop from the bottom margin
477            left : (float)
478                fraction to crop from the left margin
479            right : (float)
480                fraction to crop from the right margin
481            pixels : (bool)
482                units are pixels
483        """
484        extractVOI = vtki.new("ExtractVOI")
485        extractVOI.SetInputData(self.dataset)
486        extractVOI.IncludeBoundaryOn()
487
488        d = self.dataset.GetDimensions()
489        if pixels:
490            extractVOI.SetVOI(left, d[0] - right - 1, bottom, d[1] - top - 1, 0, 0)
491        else:
492            bx0, bx1, by0, by1 = 0, d[0]-1, 0, d[1]-1
493            if left is not None:   bx0 = int((d[0]-1)*left)
494            if right is not None:  bx1 = int((d[0]-1)*(1-right))
495            if bottom is not None: by0 = int((d[1]-1)*bottom)
496            if top is not None:    by1 = int((d[1]-1)*(1-top))
497            extractVOI.SetVOI(bx0, bx1, by0, by1, 0, 0)
498        extractVOI.Update()
499
500        self._update(extractVOI.GetOutput())
501        self.pipeline = utils.OperationNode(
502            "crop", comment=f"shape={tuple(self.shape)}", parents=[self], c="#f28482"
503        )
504        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:
506    def pad(self, pixels=10, value=255) -> Self:
507        """
508        Add the specified number of pixels at the image borders.
509        Pixels can be a list formatted as `[left, right, bottom, top]`.
510
511        Arguments:
512            pixels : (int, list)
513                number of pixels to be added (or a list of length 4)
514            value : (int)
515                intensity value (gray-scale color) of the padding
516        """
517        x0, x1, y0, y1, _z0, _z1 = self.dataset.GetExtent()
518        pf = vtki.new("ImageConstantPad")
519        pf.SetInputData(self.dataset)
520        pf.SetConstant(value)
521        if utils.is_sequence(pixels):
522            pf.SetOutputWholeExtent(
523                x0 - pixels[0], x1 + pixels[1],
524                y0 - pixels[2], y1 + pixels[3],
525                0, 0
526            )
527        else:
528            pf.SetOutputWholeExtent(
529                x0 - pixels, x1 + pixels,
530                y0 - pixels, y1 + pixels,
531                0, 0
532            )
533        pf.Update()
534        self._update(pf.GetOutput())
535        self.pipeline = utils.OperationNode(
536            "pad", comment=f"{pixels} pixels", parents=[self], c="#f28482"
537        )
538        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:
540    def tile(self, nx=4, ny=4, shift=(0, 0)) -> "Image":
541        """
542        Generate a tiling from the current image by mirroring and repeating it.
543
544        Arguments:
545            nx : (float)
546                number of repeats along x
547            ny : (float)
548                number of repeats along x
549            shift : (list)
550                shift in x and y in pixels
551        """
552        x0, x1, y0, y1, z0, z1 = self.dataset.GetExtent()
553        constant_pad = vtki.new("ImageMirrorPad")
554        constant_pad.SetInputData(self.dataset)
555        constant_pad.SetOutputWholeExtent(
556            int(x0 + shift[0] + 0.5),
557            int(x1 * nx + shift[0] + 0.5),
558            int(y0 + shift[1] + 0.5),
559            int(y1 * ny + shift[1] + 0.5),
560            z0,
561            z1,
562        )
563        constant_pad.Update()
564        img = Image(constant_pad.GetOutput())
565
566        img.pipeline = utils.OperationNode(
567            "tile", comment=f"by {nx}x{ny}", parents=[self], c="#f28482"
568        )
569        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:
571    def append(self, images: list, axis="z", preserve_extents=False) -> Self:
572        """
573        Append the input images to the current one along the specified axis.
574        Except for the append axis, all inputs must have the same extent.
575        All inputs must have the same number of scalar components.
576        The output has the same origin and spacing as the first input.
577        The origin and spacing of all other inputs are ignored.
578        All inputs must have the same scalar type.
579
580        Arguments:
581            axis : (int, str)
582                axis expanded to hold the multiple images
583            preserve_extents : (bool)
584                if True, the extent of the inputs is used to place
585                the image in the output. The whole extent of the output is the union of the input
586                whole extents. Any portion of the output not covered by the inputs is set to zero.
587                The origin and spacing is taken from the first input.
588
589        Example:
590            ```python
591            from vedo import Image, dataurl
592            pic = Image(dataurl+'dog.jpg').pad()
593            pic.append([pic, pic], axis='y')
594            pic.append([pic, pic, pic], axis='x')
595            pic.show(axes=1).close()
596            ```
597            ![](https://vedo.embl.es/images/feats/pict_append.png)
598        """
599        ima = vtki.new("ImageAppend")
600        ima.SetInputData(self.dataset)
601        for p in images:
602            if isinstance(p, vtki.vtkImageData):
603                ima.AddInputData(p)
604            else:
605                ima.AddInputData(p.dataset)
606        ima.SetPreserveExtents(preserve_extents)
607        if axis == "x":
608            axis = 0
609        elif axis == "y":
610            axis = 1
611        ima.SetAppendAxis(axis)
612        ima.Update()
613        self._update(ima.GetOutput())
614        self.pipeline = utils.OperationNode(
615            "append", comment=f"axis={axis}", parents=[self, *images], c="#f28482"
616        )
617        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:
619    def resize(self, newsize: Any) -> Self:
620        """
621        Resize the image resolution by specifying the number of pixels in width and height.
622        If left to zero, it will be automatically calculated to keep the original aspect ratio.
623
624        `newsize` is the shape of image as [npx, npy], or it can be also expressed as a fraction.
625        """
626        old_dims = np.array(self.dataset.GetDimensions())
627
628        if not utils.is_sequence(newsize):
629            newsize = (old_dims * newsize + 0.5).astype(int)
630
631        if not newsize[1]:
632            ar = old_dims[1] / old_dims[0]
633            newsize = [newsize[0], int(newsize[0] * ar + 0.5)]
634        if not newsize[0]:
635            ar = old_dims[0] / old_dims[1]
636            newsize = [int(newsize[1] * ar + 0.5), newsize[1]]
637        newsize = [newsize[0], newsize[1], old_dims[2]]
638
639        rsz = vtki.new("ImageResize")
640        rsz.SetInputData(self.dataset)
641        rsz.SetResizeMethodToOutputDimensions()
642        rsz.SetOutputDimensions(newsize)
643        rsz.Update()
644        out = rsz.GetOutput()
645        out.SetSpacing(1, 1, 1)
646        self._update(out)
647        self.pipeline = utils.OperationNode(
648            "resize", comment=f"shape={tuple(self.shape)}", parents=[self], c="#f28482"
649        )
650        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:
652    def mirror(self, axis="x") -> Self:
653        """Mirror image along x or y axis. Same as `flip()`."""
654        ff = vtki.new("ImageFlip")
655        ff.SetInputData(self.dataset)
656        if axis.lower() == "x":
657            ff.SetFilteredAxis(0)
658        elif axis.lower() == "y":
659            ff.SetFilteredAxis(1)
660        else:
661            colors.printc("Error in mirror(): mirror must be set to x or y.", c="r")
662            raise RuntimeError()
663        ff.Update()
664        self._update(ff.GetOutput())
665        self.pipeline = utils.OperationNode(f"mirror {axis}", parents=[self], c="#f28482")
666        return self

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

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

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

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

Select one single component of the rgb image.

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

Make it black and white using luminance calibration.

def smooth(self, sigma=3, radius=None) -> Self:
703    def smooth(self, sigma=3, radius=None) -> Self:
704        """
705        Smooth a `Image` with Gaussian kernel.
706
707        Arguments:
708            sigma : (int)
709                number of sigmas in pixel units
710            radius : (float)
711                how far out the gaussian kernel will go before being clamped to zero
712        """
713        gsf = vtki.new("ImageGaussianSmooth")
714        gsf.SetDimensionality(2)
715        gsf.SetInputData(self.dataset)
716        if radius is not None:
717            if utils.is_sequence(radius):
718                gsf.SetRadiusFactors(radius[0], radius[1])
719            else:
720                gsf.SetRadiusFactor(radius)
721
722        if utils.is_sequence(sigma):
723            gsf.SetStandardDeviations(sigma[0], sigma[1])
724        else:
725            gsf.SetStandardDeviation(sigma)
726        gsf.Update()
727        self._update(gsf.GetOutput())
728        self.pipeline = utils.OperationNode(
729            "smooth", comment=f"sigma={sigma}", parents=[self], c="#f28482"
730        )
731        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:
733    def median(self) -> Self:
734        """
735        Median filter that preserves thin lines and corners.
736
737        It operates on a 5x5 pixel neighborhood. It computes two values initially:
738        the median of the + neighbors and the median of the x neighbors.
739        It then computes the median of these two values plus the center pixel.
740        This result of this second median is the output pixel value.
741        """
742        medf = vtki.new("ImageHybridMedian2D")
743        medf.SetInputData(self.dataset)
744        medf.Update()
745        self._update(medf.GetOutput())
746        self.pipeline = utils.OperationNode("median", parents=[self], c="#f28482")
747        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:
749    def enhance(self) -> Self:
750        """
751        Enhance a b&w image using the laplacian, enhancing high-freq edges.
752
753        Example:
754            ```python
755            from vedo import *
756            pic = Image(dataurl+'images/dog.jpg').bw()
757            show(pic, pic.clone().enhance(), N=2, mode='image', zoom='tight')
758            ```
759            ![](https://vedo.embl.es/images/feats/pict_enhance.png)
760        """
761        img = self.dataset
762        scalarRange = img.GetPointData().GetScalars().GetRange()
763
764        cast = vtki.new("ImageCast")
765        cast.SetInputData(img)
766        cast.SetOutputScalarTypeToDouble()
767        cast.Update()
768
769        laplacian = vtki.new("ImageLaplacian")
770        laplacian.SetInputData(cast.GetOutput())
771        laplacian.SetDimensionality(2)
772        laplacian.Update()
773
774        subtr = vtki.new("ImageMathematics")
775        subtr.SetInputData(0, cast.GetOutput())
776        subtr.SetInputData(1, laplacian.GetOutput())
777        subtr.SetOperationToSubtract()
778        subtr.Update()
779
780        color_window = scalarRange[1] - scalarRange[0]
781        color_level = color_window / 2
782        original_color = vtki.new("ImageMapToWindowLevelColors")
783        original_color.SetWindow(color_window)
784        original_color.SetLevel(color_level)
785        original_color.SetInputData(subtr.GetOutput())
786        original_color.Update()
787        self._update(original_color.GetOutput())
788
789        self.pipeline = utils.OperationNode("enhance", parents=[self], c="#f28482")
790        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:
792    def fft(self, mode="magnitude", logscale=12, center=True) -> "Image":
793        """
794        Fast Fourier transform of a image.
795
796        Arguments:
797            logscale : (float)
798                if non-zero, take the logarithm of the intensity and scale it by this factor.
799            mode : (str)
800                either [magnitude, real, imaginary, complex], compute the point array data accordingly.
801            center : (bool)
802                shift constant zero-frequency to the center of the image for display.
803                (FFT converts spatial images into frequency space, but puts the zero frequency at the origin)
804        """
805        ffti = vtki.new("ImageFFT")
806        ffti.SetInputData(self.dataset)
807        ffti.Update()
808
809        if "mag" in mode:
810            mag = vtki.new("ImageMagnitude")
811            mag.SetInputData(ffti.GetOutput())
812            mag.Update()
813            out = mag.GetOutput()
814        elif "real" in mode:
815            erf = vtki.new("ImageExtractComponents")
816            erf.SetInputData(ffti.GetOutput())
817            erf.SetComponents(0)
818            erf.Update()
819            out = erf.GetOutput()
820        elif "imaginary" in mode:
821            eimf = vtki.new("ImageExtractComponents")
822            eimf.SetInputData(ffti.GetOutput())
823            eimf.SetComponents(1)
824            eimf.Update()
825            out = eimf.GetOutput()
826        elif "complex" in mode:
827            out = ffti.GetOutput()
828        else:
829            colors.printc("Error in fft(): unknown mode", mode)
830            raise RuntimeError()
831
832        if center:
833            center = vtki.new("ImageFourierCenter")
834            center.SetInputData(out)
835            center.Update()
836            out = center.GetOutput()
837
838        if "complex" not in mode:
839            if logscale:
840                ils = vtki.new("ImageLogarithmicScale")
841                ils.SetInputData(out)
842                ils.SetConstant(logscale)
843                ils.Update()
844                out = ils.GetOutput()
845
846        pic = Image(out)
847        pic.pipeline = utils.OperationNode("FFT", parents=[self], c="#f28482")
848        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:
850    def rfft(self, mode="magnitude") -> "Image":
851        """Reverse Fast Fourier transform of a image."""
852
853        ffti = vtki.new("ImageRFFT")
854        ffti.SetInputData(self.dataset)
855        ffti.Update()
856
857        if "mag" in mode:
858            mag = vtki.new("ImageMagnitude")
859            mag.SetInputData(ffti.GetOutput())
860            mag.Update()
861            out = mag.GetOutput()
862        elif "real" in mode:
863            erf = vtki.new("ImageExtractComponents")
864            erf.SetInputData(ffti.GetOutput())
865            erf.SetComponents(0)
866            erf.Update()
867            out = erf.GetOutput()
868        elif "imaginary" in mode:
869            eimf = vtki.new("ImageExtractComponents")
870            eimf.SetInputData(ffti.GetOutput())
871            eimf.SetComponents(1)
872            eimf.Update()
873            out = eimf.GetOutput()
874        elif "complex" in mode:
875            out = ffti.GetOutput()
876        else:
877            colors.printc("Error in rfft(): unknown mode", mode)
878            raise RuntimeError()
879
880        pic = Image(out)
881        pic.pipeline = utils.OperationNode("rFFT", parents=[self], c="#f28482")
882        return pic

Reverse Fast Fourier transform of a image.

def filterpass(self, lowcutoff=None, highcutoff=None, order=3) -> Self:
884    def filterpass(self, lowcutoff=None, highcutoff=None, order=3) -> Self:
885        """
886        Low-pass and high-pass filtering become trivial in the frequency domain.
887        A portion of the pixels/voxels are simply masked or attenuated.
888        This function applies a high pass Butterworth filter that attenuates the
889        frequency domain image with the function
890
891        The gradual attenuation of the filter is important.
892        A simple high-pass filter would simply mask a set of pixels in the frequency domain,
893        but the abrupt transition would cause a ringing effect in the spatial domain.
894
895        Arguments:
896            lowcutoff : (list)
897                the cutoff frequencies
898            highcutoff : (list)
899                the cutoff frequencies
900            order : (int)
901                order determines sharpness of the cutoff curve
902        """
903        # https://lorensen.github.io/VTKExamples/site/Cxx/ImageProcessing/IdealHighPass
904        fft = vtki.new("ImageFFT")
905        fft.SetInputData(self.dataset)
906        fft.Update()
907        out = fft.GetOutput()
908
909        if highcutoff:
910            blp = vtki.new("ImageButterworthLowPass")
911            blp.SetInputData(out)
912            blp.SetCutOff(highcutoff)
913            blp.SetOrder(order)
914            blp.Update()
915            out = blp.GetOutput()
916
917        if lowcutoff:
918            bhp = vtki.new("ImageButterworthHighPass")
919            bhp.SetInputData(out)
920            bhp.SetCutOff(lowcutoff)
921            bhp.SetOrder(order)
922            bhp.Update()
923            out = bhp.GetOutput()
924
925        rfft = vtki.new("ImageRFFT")
926        rfft.SetInputData(out)
927        rfft.Update()
928
929        ecomp = vtki.new("ImageExtractComponents")
930        ecomp.SetInputData(rfft.GetOutput())
931        ecomp.SetComponents(0)
932        ecomp.Update()
933
934        caster = vtki.new("ImageCast")
935        caster.SetOutputScalarTypeToUnsignedChar()
936        caster.SetInputData(ecomp.GetOutput())
937        caster.Update()
938        self._update(caster.GetOutput())
939        self.pipeline = utils.OperationNode("filterpass", parents=[self], c="#f28482")
940        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:
942    def blend(self, pic, alpha1=0.5, alpha2=0.5) -> Self:
943        """
944        Take L, LA, RGB, or RGBA images as input and blends
945        them according to the alpha values and/or the opacity setting for each input.
946        """
947        blf = vtki.new("ImageBlend")
948        blf.AddInputData(self.dataset)
949        blf.AddInputData(pic.dataset)
950        blf.SetOpacity(0, alpha1)
951        blf.SetOpacity(1, alpha2)
952        blf.SetBlendModeToNormal()
953        blf.Update()
954        self._update(blf.GetOutput())
955        self.pipeline = utils.OperationNode("blend", parents=[self, pic], c="#f28482")
956        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:
 958    def warp(
 959        self,
 960        source_pts=(),
 961        target_pts=(),
 962        transform=None,
 963        sigma=1,
 964        mirroring=False,
 965        bc="w",
 966        alpha=1,
 967    ) -> Self:
 968        """
 969        Warp an image using thin-plate splines.
 970
 971        Arguments:
 972            source_pts : (list)
 973                source points
 974            target_pts : (list)
 975                target points
 976            transform : (vtkTransform)
 977                a vtkTransform object can be supplied
 978            sigma : (float), optional
 979                stiffness of the interpolation
 980            mirroring : (bool)
 981                fill the margins with a reflection of the original image
 982            bc : (color)
 983                fill the margins with a solid color
 984            alpha : (float)
 985                opacity of the filled margins
 986        """
 987        if transform is None:
 988            # source and target must be filled
 989            transform = vtki.vtkThinPlateSplineTransform()
 990            transform.SetBasisToR2LogR()
 991
 992            parents = [self]
 993            if isinstance(source_pts, vedo.Points):
 994                parents.append(source_pts)
 995                source_pts = source_pts.vertices
 996            if isinstance(target_pts, vedo.Points):
 997                parents.append(target_pts)
 998                target_pts = target_pts.vertices
 999
1000            ns = len(source_pts)
1001            nt = len(target_pts)
1002            if ns != nt:
1003                colors.printc("Error in image.warp(): #source != #target points", ns, nt, c="r")
1004                raise RuntimeError()
1005
1006            ptsou = vtki.vtkPoints()
1007            ptsou.SetNumberOfPoints(ns)
1008
1009            pttar = vtki.vtkPoints()
1010            pttar.SetNumberOfPoints(nt)
1011
1012            for i in range(ns):
1013                p = source_pts[i]
1014                ptsou.SetPoint(i, [p[0], p[1], 0])
1015                p = target_pts[i]
1016                pttar.SetPoint(i, [p[0], p[1], 0])
1017
1018            transform.SetSigma(sigma)
1019            transform.SetSourceLandmarks(pttar)
1020            transform.SetTargetLandmarks(ptsou)
1021        else:
1022            # ignore source and target
1023            pass
1024
1025        reslice = vtki.new("ImageReslice")
1026        reslice.SetInputData(self.dataset)
1027        reslice.SetOutputDimensionality(2)
1028        reslice.SetResliceTransform(transform)
1029        reslice.SetInterpolationModeToCubic()
1030        reslice.SetMirror(mirroring)
1031        c = np.array(colors.get_color(bc)) * 255
1032        reslice.SetBackgroundColor([c[0], c[1], c[2], alpha * 255])
1033        reslice.Update()
1034        self._update(reslice.GetOutput())
1035        self.pipeline = utils.OperationNode("warp", parents=parents, c="#f28482")
1036        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:
1038    def invert(self) -> Self:
1039        """
1040        Return an inverted image (inverted in each color channel).
1041        """
1042        rgb = self.tonumpy()
1043        data = 255 - np.array(rgb)
1044        self._update(_get_img(data))
1045        self.pipeline = utils.OperationNode("invert", parents=[self], c="#f28482")
1046        return self

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

def binarize(self, threshold=None, invert=False) -> Self:
1048    def binarize(self, threshold=None, invert=False) -> Self:
1049        """
1050        Return a new Image where pixel above threshold are set to 255
1051        and pixels below are set to 0.
1052
1053        Arguments:
1054            threshold : (float)
1055                input threshold value
1056            invert : (bool)
1057                invert threshold direction
1058
1059        Example:
1060        ```python
1061        from vedo import Image, show
1062        pic1 = Image("https://aws.glamour.es/prod/designs/v1/assets/620x459/547577.jpg")
1063        pic2 = pic1.clone().invert()
1064        pic3 = pic1.clone().binarize()
1065        show(pic1, pic2, pic3, N=3, bg="blue9").close()
1066        ```
1067        ![](https://vedo.embl.es/images/feats/pict_binarize.png)
1068        """
1069        rgb = self.tonumpy()
1070        if rgb.ndim == 3:
1071            intensity = np.sum(rgb, axis=2) / 3
1072        else:
1073            intensity = rgb
1074
1075        if threshold is None:
1076            vmin, vmax = np.min(intensity), np.max(intensity)
1077            threshold = (vmax + vmin) / 2
1078
1079        data = np.zeros_like(intensity).astype(np.uint8)
1080        mask = np.where(intensity > threshold)
1081        if invert:
1082            data += 255
1083            data[mask] = 0
1084        else:
1085            data[mask] = 255
1086
1087        self._update(_get_img(data, flip=True))
1088
1089        self.pipeline = utils.OperationNode(
1090            "binarize", comment=f"threshold={threshold}", parents=[self], c="#f28482"
1091        )
1092        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:
1094    def threshold(self, value=None, flip=False) -> "vedo.Mesh":
1095        """
1096        Create a polygonal Mesh from a Image by filling regions with pixels
1097        luminosity above a specified value.
1098
1099        Arguments:
1100            value : (float)
1101                The default is None, e.i. 1/3 of the scalar range.
1102            flip: (bool)
1103                Flip polygon orientations
1104
1105        Returns:
1106            A polygonal mesh.
1107        """
1108        mgf = vtki.new("ImageMagnitude")
1109        mgf.SetInputData(self.dataset)
1110        mgf.Update()
1111        msq = vtki.new("MarchingSquares")
1112        msq.SetInputData(mgf.GetOutput())
1113        if value is None:
1114            r0, r1 = self.dataset.GetScalarRange()
1115            value = r0 + (r1 - r0) / 3
1116        msq.SetValue(0, value)
1117        msq.Update()
1118        if flip:
1119            rs = vtki.new("ReverseSense")
1120            rs.SetInputData(msq.GetOutput())
1121            rs.ReverseCellsOn()
1122            rs.ReverseNormalsOff()
1123            rs.Update()
1124            output = rs.GetOutput()
1125        else:
1126            output = msq.GetOutput()
1127        ctr = vtki.new("ContourTriangulator")
1128        ctr.SetInputData(output)
1129        ctr.Update()
1130        out = vedo.Mesh(ctr.GetOutput(), c="k").bc("t").lighting("off")
1131
1132        out.pipeline = utils.OperationNode(
1133            "threshold", comment=f"{value: .2f}", parents=[self], c="#f28482:#e9c46a"
1134        )
1135        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:
1137    def cmap(self, name: str, vmin=None, vmax=None) -> Self:
1138        """Colorize a image with a colormap representing pixel intensity"""
1139        n = self.dataset.GetPointData().GetNumberOfComponents()
1140        if n > 1:
1141            ecr = vtki.new("ImageExtractComponents")
1142            ecr.SetInputData(self.dataset)
1143            ecr.SetComponents(0, 1, 2)
1144            ecr.Update()
1145            ilum = vtki.new("ImageMagnitude")
1146            ilum.SetInputData(self.dataset)
1147            ilum.Update()
1148            img = ilum.GetOutput()
1149        else:
1150            img = self.dataset
1151
1152        lut = vtki.vtkLookupTable()
1153        _vmin, _vmax = img.GetScalarRange()
1154        if vmin is not None:
1155            _vmin = vmin
1156        if vmax is not None:
1157            _vmax = vmax
1158        lut.SetRange(_vmin, _vmax)
1159
1160        ncols = 256
1161        lut.SetNumberOfTableValues(ncols)
1162        cols = colors.color_map(range(ncols), name, 0, ncols)
1163        for i, c in enumerate(cols):
1164            lut.SetTableValue(i, *c)
1165        lut.Build()
1166
1167        imap = vtki.new("ImageMapToColors")
1168        imap.SetLookupTable(lut)
1169        imap.SetInputData(img)
1170        imap.Update()
1171        self._update(imap.GetOutput())
1172        self.pipeline = utils.OperationNode(
1173            "cmap", comment=f'"{name}"', parents=[self], c="#f28482"
1174        )
1175        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:
1177    def rotate(self, angle: float, center=(), scale=1.0, mirroring=False, bc="w", alpha=1.0) -> Self:
1178        """
1179        Rotate by the specified angle (anticlockwise).
1180
1181        Arguments:
1182            angle : (float)
1183                rotation angle in degrees
1184            center : (list)
1185                center of rotation (x,y) in pixels
1186        """
1187        bounds = self.bounds()
1188        pc = [0, 0, 0]
1189        if center:
1190            pc[0] = center[0]
1191            pc[1] = center[1]
1192        else:
1193            pc[0] = (bounds[1] + bounds[0]) / 2.0
1194            pc[1] = (bounds[3] + bounds[2]) / 2.0
1195        pc[2] = (bounds[5] + bounds[4]) / 2.0
1196
1197        transform = vtki.vtkTransform()
1198        transform.Translate(pc)
1199        transform.RotateWXYZ(-angle, 0, 0, 1)
1200        transform.Scale(1 / scale, 1 / scale, 1)
1201        transform.Translate(-pc[0], -pc[1], -pc[2])
1202
1203        reslice = vtki.new("ImageReslice")
1204        reslice.SetMirror(mirroring)
1205        c = np.array(colors.get_color(bc)) * 255
1206        reslice.SetBackgroundColor([c[0], c[1], c[2], alpha * 255])
1207        reslice.SetInputData(self.dataset)
1208        reslice.SetResliceTransform(transform)
1209        reslice.SetOutputDimensionality(2)
1210        reslice.SetInterpolationModeToCubic()
1211        reslice.AutoCropOutputOn()
1212        reslice.Update()
1213        self._update(reslice.GetOutput())
1214
1215        self.pipeline = utils.OperationNode(
1216            "rotate", comment=f"angle={angle}", parents=[self], c="#f28482"
1217        )
1218        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:
1220    def tomesh(self) -> "vedo.shapes.Grid":
1221        """
1222        Convert an image to polygonal data (quads),
1223        with each polygon vertex assigned a RGBA value.
1224        """
1225        dims = self.dataset.GetDimensions()
1226        gr = vedo.shapes.Grid(s=dims[:2], res=(dims[0] - 1, dims[1] - 1))
1227        gr.pos(int(dims[0] / 2), int(dims[1] / 2)).pickable(True).wireframe(False).lw(0)
1228        self.dataset.GetPointData().GetScalars().SetName("RGBA")
1229        gr.dataset.GetPointData().AddArray(self.dataset.GetPointData().GetScalars())
1230        gr.dataset.GetPointData().SetActiveScalars("RGBA")
1231        gr.mapper.SetArrayName("RGBA")
1232        gr.mapper.SetScalarModeToUsePointData()
1233        gr.mapper.ScalarVisibilityOn()
1234        gr.name = self.name
1235        gr.filename = self.filename
1236        gr.pipeline = utils.OperationNode("tomesh", parents=[self], c="#f28482:#e9c46a")
1237        return gr

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

def tonumpy(self) -> numpy.ndarray:
1239    def tonumpy(self) -> np.ndarray:
1240        """
1241        Get read-write access to pixels of a Image object as a numpy array.
1242        Note that the shape is (nrofchannels, nx, ny).
1243
1244        When you set values in the output image, you don't want numpy to reallocate the array
1245        but instead set values in the existing array, so use the [:] operator.
1246        Example: arr[:] = arr - 15
1247
1248        If the array is modified call:
1249        `image.modified()`
1250        when all your modifications are completed.
1251        """
1252        nx, ny, _ = self.dataset.GetDimensions()
1253        nchan = self.dataset.GetPointData().GetScalars().GetNumberOfComponents()
1254        narray = utils.vtk2numpy(self.dataset.GetPointData().GetScalars()).reshape(ny, nx, nchan)
1255        narray = np.flip(narray, axis=0).astype(np.uint8)
1256        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:
1258    def add_rectangle(self, xspan: List[float], yspan: List[float], c="green5", alpha=1.0) -> Self:
1259        """Draw a rectangle box on top of current image. Units are pixels.
1260
1261        Example:
1262            ```python
1263            import vedo
1264            pic = vedo.Image(vedo.dataurl+"images/dog.jpg")
1265            pic.add_rectangle([100,300], [100,200], c='green4', alpha=0.7)
1266            pic.add_line([100,100],[400,500], lw=2, alpha=1)
1267            pic.add_triangle([250,300], [100,300], [200,400], c='blue5')
1268            vedo.show(pic, axes=1).close()
1269            ```
1270            ![](https://vedo.embl.es/images/feats/pict_drawon.png)
1271        """
1272        x1, x2 = xspan
1273        y1, y2 = yspan
1274
1275        r, g, b = vedo.colors.get_color(c)
1276        c = np.array([r, g, b]) * 255
1277        c = c.astype(np.uint8)
1278
1279        alpha = min(alpha, 1)
1280        if alpha <= 0:
1281            return self
1282        alpha2 = alpha
1283        alpha1 = 1 - alpha
1284
1285        nx, ny = self.dimensions()
1286        if x2 > nx:
1287            x2 = nx - 1
1288        if y2 > ny:
1289            y2 = ny - 1
1290
1291        nchan = self.channels
1292        narrayA = self.tonumpy()
1293
1294        canvas_source = vtki.new("ImageCanvasSource2D")
1295        canvas_source.SetExtent(0, nx - 1, 0, ny - 1, 0, 0)
1296        canvas_source.SetScalarTypeToUnsignedChar()
1297        canvas_source.SetNumberOfScalarComponents(nchan)
1298        canvas_source.SetDrawColor(255, 255, 255)
1299        canvas_source.FillBox(x1, x2, y1, y2)
1300        canvas_source.Update()
1301        imagedataset = canvas_source.GetOutput()
1302
1303        vscals = imagedataset.GetPointData().GetScalars()
1304        narrayB = vedo.utils.vtk2numpy(vscals).reshape(ny, nx, nchan)
1305        narrayB = np.flip(narrayB, axis=0)
1306        narrayC = np.where(narrayB < 255, narrayA, alpha1 * narrayA + alpha2 * c)
1307        self._update(_get_img(narrayC))
1308        self.pipeline = utils.OperationNode("rectangle", parents=[self], c="#f28482")
1309        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:
1311    def add_line(self, p1: List[float], p2: List[float], lw=2, c="k2", alpha=1.0) -> Self:
1312        """Draw a line on top of current image. Units are pixels."""
1313        x1, x2 = p1
1314        y1, y2 = p2
1315
1316        r, g, b = vedo.colors.get_color(c)
1317        c = np.array([r, g, b]) * 255
1318        c = c.astype(np.uint8)
1319
1320        alpha = min(alpha, 1)
1321        if alpha <= 0:
1322            return self
1323        alpha2 = alpha
1324        alpha1 = 1 - alpha
1325
1326        nx, ny = self.dimensions()
1327        if x2 > nx:
1328            x2 = nx - 1
1329        if y2 > ny:
1330            y2 = ny - 1
1331
1332        nchan = self.channels
1333        narrayA = self.tonumpy()
1334
1335        canvas_source = vtki.new("ImageCanvasSource2D")
1336        canvas_source.SetExtent(0, nx - 1, 0, ny - 1, 0, 0)
1337        canvas_source.SetScalarTypeToUnsignedChar()
1338        canvas_source.SetNumberOfScalarComponents(nchan)
1339        canvas_source.SetDrawColor(255, 255, 255)
1340        canvas_source.FillTube(x1, x2, y1, y2, lw)
1341        canvas_source.Update()
1342        imagedataset = canvas_source.GetOutput()
1343
1344        vscals = imagedataset.GetPointData().GetScalars()
1345        narrayB = vedo.utils.vtk2numpy(vscals).reshape(ny, nx, nchan)
1346        narrayB = np.flip(narrayB, axis=0)
1347        narrayC = np.where(narrayB < 255, narrayA, alpha1 * narrayA + alpha2 * c)
1348        self._update(_get_img(narrayC))
1349        self.pipeline = utils.OperationNode("line", parents=[self], c="#f28482")
1350        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:
1352    def add_triangle(self, p1: List[float], p2: List[float], p3: List[float], c="red3", alpha=1.0) -> Self:
1353        """Draw a triangle on top of current image. Units are pixels."""
1354        x1, y1 = p1
1355        x2, y2 = p2
1356        x3, y3 = p3
1357
1358        r, g, b = vedo.colors.get_color(c)
1359        c = np.array([r, g, b]) * 255
1360        c = c.astype(np.uint8)
1361
1362        alpha = min(alpha, 1)
1363        if alpha <= 0:
1364            return self
1365        alpha2 = alpha
1366        alpha1 = 1 - alpha
1367
1368        nx, ny = self.dimensions()
1369        x1 = min(x1, nx)
1370        x2 = min(x2, nx)
1371        x3 = min(x3, nx)
1372
1373        y1 = min(y1, ny)
1374        y2 = min(y2, ny)
1375        y3 = min(y3, ny)
1376
1377        nchan = self.channels
1378        narrayA = self.tonumpy()
1379
1380        canvas_source = vtki.new("ImageCanvasSource2D")
1381        canvas_source.SetExtent(0, nx - 1, 0, ny - 1, 0, 0)
1382        canvas_source.SetScalarTypeToUnsignedChar()
1383        canvas_source.SetNumberOfScalarComponents(nchan)
1384        canvas_source.SetDrawColor(255, 255, 255)
1385        canvas_source.FillTriangle(x1, y1, x2, y2, x3, y3)
1386        canvas_source.Update()
1387        imagedataset = canvas_source.GetOutput()
1388
1389        vscals = imagedataset.GetPointData().GetScalars()
1390        narrayB = vedo.utils.vtk2numpy(vscals).reshape(ny, nx, nchan)
1391        narrayB = np.flip(narrayB, axis=0)
1392        narrayC = np.where(narrayB < 255, narrayA, alpha1 * narrayA + alpha2 * c)
1393        self._update(_get_img(narrayC))
1394        self.pipeline = utils.OperationNode("triangle", parents=[self], c="#f28482")
1395        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:
1397    def add_text(
1398        self,
1399        txt: str,
1400        width=400,
1401        height=200,
1402        alpha=1,
1403        c="black",
1404        bg=None,
1405        alpha_bg=1,
1406        font="Theemim",
1407        dpi=200,
1408        justify="bottom-left",
1409    ) -> Self:
1410        """Add text to an image."""
1411
1412        tp = vtki.vtkTextProperty()
1413        tp.BoldOff()
1414        tp.FrameOff()
1415        tp.SetColor(colors.get_color(c))
1416        tp.SetJustificationToLeft()
1417        if "top" in justify:
1418            tp.SetVerticalJustificationToTop()
1419        if "bottom" in justify:
1420            tp.SetVerticalJustificationToBottom()
1421        if "cent" in justify:
1422            tp.SetVerticalJustificationToCentered()
1423            tp.SetJustificationToCentered()
1424        if "left" in justify:
1425            tp.SetJustificationToLeft()
1426        if "right" in justify:
1427            tp.SetJustificationToRight()
1428
1429        if   font.lower() == "courier": tp.SetFontFamilyToCourier()
1430        elif font.lower() == "times": tp.SetFontFamilyToTimes()
1431        elif font.lower() == "arial": tp.SetFontFamilyToArial()
1432        else:
1433            tp.SetFontFamily(vtki.VTK_FONT_FILE)
1434            tp.SetFontFile(utils.get_font_path(font))
1435
1436        if bg:
1437            bgcol = colors.get_color(bg)
1438            tp.SetBackgroundColor(bgcol)
1439            tp.SetBackgroundOpacity(alpha_bg)
1440            tp.SetFrameColor(bgcol)
1441            tp.FrameOn()
1442
1443        tr = vtki.new("TextRenderer")
1444        # GetConstrainedFontSize (const vtkUnicodeString &str,
1445        # vtkTextProperty(*tprop, int targetWidth, int targetHeight, int dpi)
1446        fs = tr.GetConstrainedFontSize(txt, tp, width, height, dpi)
1447        tp.SetFontSize(fs)
1448
1449        img = vtki.vtkImageData()
1450        # img.SetOrigin(*pos,1)
1451        tr.RenderString(tp, txt, img, [width, height], dpi)
1452        # RenderString (vtkTextProperty *tprop, const vtkStdString &str,
1453        #   vtkImageData *data, int textDims[2], int dpi, int backend=Default)
1454
1455        blf = vtki.new("ImageBlend")
1456        blf.AddInputData(self.dataset)
1457        blf.AddInputData(img)
1458        blf.SetOpacity(0, 1)
1459        blf.SetOpacity(1, alpha)
1460        blf.SetBlendModeToNormal()
1461        blf.Update()
1462
1463        self._update(blf.GetOutput())
1464        self.pipeline = utils.OperationNode(
1465            "add_text", comment=f"{txt}", parents=[self], c="#f28482"
1466        )
1467        return self

Add text to an image.

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

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

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

Write image to file as png or jpg.

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

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

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

Deprecated. Use Image instead.