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

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

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

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):
364    def dimensions(self):
365        """
366        Return the image dimension as number of pixels in x and y. 
367        Alias of property `shape`.
368        """
369        nx, ny, _ = self.dataset.GetDimensions()
370        return np.array([nx, ny])

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

shape

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

def channels(self):
377    def channels(self):
378        """Return the number of channels in image"""
379        return self.dataset.GetPointData().GetScalars().GetNumberOfComponents()

Return the number of channels in image

def copy(self):
381    def copy(self):
382        """Return a copy of the image. Alias of `clone()`."""
383        return self.clone()

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

def clone(self):
385    def clone(self):
386        """Return an exact copy of the input Image.
387        If transform is True, it is given the same scaling and position."""
388        img = vtk.vtkImageData()
389        img.DeepCopy(self.dataset)
390
391        pic = Image(img)
392        # assign the same transformation to the copy
393        pic.actor.SetOrigin(self.actor.GetOrigin())
394        pic.actor.SetScale(self.actor.GetScale())
395        pic.actor.SetOrientation(self.actor.GetOrientation())
396        pic.actor.SetPosition(self.actor.GetPosition())
397
398        pic.pipeline = utils.OperationNode("clone", parents=[self], c="#f7dada", shape="diamond")
399        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), scale=1, justify=''):
401    def clone2d(self, pos=(0, 0), scale=1, justify=""):
402        """
403        Embed an image as a static 2D image in the canvas.
404        
405        Return a 2D (an `Actor2D`) copy of the input Image.
406        
407        Arguments:
408            pos : (list, str)
409                2D (x,y) position in range [0,1],
410                [0,0] being the bottom-left corner  
411            scale : (float)
412                apply a scaling factor to the image
413            justify : (str)
414                define the anchor point ("top-left", "top-center", ...)
415        """
416        pic = vedo.visual.Actor2D()
417
418        pic.name = self.name
419        pic.filename = self.filename
420        pic.file_size = self.file_size
421        
422        pic.dataset = self.dataset
423
424        pic.properties = pic.GetProperty()
425        pic.properties.SetDisplayLocationToBackground()
426
427        if scale != 1:
428            newsize = np.array(self.dataset.GetDimensions()[:2]) * scale
429            newsize = newsize.astype(int)
430            rsz = vtk.new("ImageResize")
431            rsz.SetInputData(self.dataset)
432            rsz.SetResizeMethodToOutputDimensions()
433            rsz.SetOutputDimensions(newsize[0], newsize[1], 1)
434            rsz.Update()
435            pic.dataset = rsz.GetOutput()
436
437        if justify:
438            pic.dataset, pos = _set_justification(pic.dataset, justify)
439        else:
440            pic.dataset, pos = _set_justification(pic.dataset, pos)
441
442        pic.mapper = vtk.new("ImageMapper")
443        pic.SetMapper(pic.mapper)
444        pic.mapper.SetInputData(pic.dataset)
445        pic.mapper.SetColorWindow(255)
446        pic.mapper.SetColorLevel(127.5)
447
448        pic.GetPositionCoordinate().SetCoordinateSystem(3)
449        pic.SetPosition(pos)
450
451        pic.shape = tuple(self.dataset.GetDimensions()[:2])
452
453        pic.pipeline = utils.OperationNode("clone2d", parents=[self], c="#f7dada", shape="diamond")
454        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
  • scale : (float) apply a scaling factor to the image
  • justify : (str) define the anchor point ("top-left", "top-center", ...)
def extent(self, ext=None):
457    def extent(self, ext=None):
458        """
459        Get or set the physical extent that the image spans.
460        Format is `ext=[minx, maxx, miny, maxy]`.
461        """
462        if ext is None:
463            return self.dataset.GetExtent()
464
465        self.dataset.SetExtent(ext[0], ext[1], ext[2], ext[3], 0, 0)
466        self.mapper.Modified()
467        return self

Get or set the physical extent that the image spans. Format is ext=[minx, maxx, miny, maxy].

def crop(self, top=None, bottom=None, right=None, left=None, pixels=False):
469    def crop(self, top=None, bottom=None, right=None, left=None, pixels=False):
470        """
471        Crop image.
472
473        Arguments:
474            top : (float)
475                fraction to crop from the top margin
476            bottom : (float)
477                fraction to crop from the bottom margin
478            left : (float)
479                fraction to crop from the left margin
480            right : (float)
481                fraction to crop from the right margin
482            pixels : (bool)
483                units are pixels
484        """
485        extractVOI = vtk.new("ExtractVOI")
486        extractVOI.SetInputData(self.dataset)
487        extractVOI.IncludeBoundaryOn()
488
489        d = self.dataset.GetDimensions()
490        if pixels:
491            extractVOI.SetVOI(left, d[0] - right - 1, bottom, d[1] - top - 1, 0, 0)
492        else:
493            bx0, bx1, by0, by1 = 0, d[0]-1, 0, d[1]-1
494            if left is not None:   bx0 = int((d[0]-1)*left)
495            if right is not None:  bx1 = int((d[0]-1)*(1-right))
496            if bottom is not None: by0 = int((d[1]-1)*bottom)
497            if top is not None:    by1 = int((d[1]-1)*(1-top))
498            extractVOI.SetVOI(bx0, bx1, by0, by1, 0, 0)
499        extractVOI.Update()
500
501        self._update(extractVOI.GetOutput())
502        self.pipeline = utils.OperationNode(
503            "crop", comment=f"shape={tuple(self.shape)}", parents=[self], c="#f28482"
504        )
505        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):
507    def pad(self, pixels=10, value=255):
508        """
509        Add the specified number of pixels at the image borders.
510        Pixels can be a list formatted as `[left, right, bottom, top]`.
511
512        Arguments:
513            pixels : (int, list)
514                number of pixels to be added (or a list of length 4)
515            value : (int)
516                intensity value (gray-scale color) of the padding
517        """
518        x0, x1, y0, y1, _z0, _z1 = self.dataset.GetExtent()
519        pf = vtk.new("ImageConstantPad")
520        pf.SetInputData(self.dataset)
521        pf.SetConstant(value)
522        if utils.is_sequence(pixels):
523            pf.SetOutputWholeExtent(
524                x0 - pixels[0], x1 + pixels[1],
525                y0 - pixels[2], y1 + pixels[3],
526                0, 0
527            )
528        else:
529            pf.SetOutputWholeExtent(
530                x0 - pixels, x1 + pixels,
531                y0 - pixels, y1 + pixels,
532                0, 0
533            )
534        pf.Update()
535        self._update(pf.GetOutput())
536        self.pipeline = utils.OperationNode(
537            "pad", comment=f"{pixels} pixels", parents=[self], c="#f28482"
538        )
539        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)):
541    def tile(self, nx=4, ny=4, shift=(0, 0)):
542        """
543        Generate a tiling from the current image by mirroring and repeating it.
544
545        Arguments:
546            nx : (float)
547                number of repeats along x
548            ny : (float)
549                number of repeats along x
550            shift : (list)
551                shift in x and y in pixels
552        """
553        x0, x1, y0, y1, z0, z1 = self.dataset.GetExtent()
554        constant_pad = vtk.new("ImageMirrorPad")
555        constant_pad.SetInputData(self.dataset)
556        constant_pad.SetOutputWholeExtent(
557            int(x0 + shift[0] + 0.5),
558            int(x1 * nx + shift[0] + 0.5),
559            int(y0 + shift[1] + 0.5),
560            int(y1 * ny + shift[1] + 0.5),
561            z0,
562            z1,
563        )
564        constant_pad.Update()
565        pic = Image(constant_pad.GetOutput())
566
567        pic.pipeline = utils.OperationNode(
568            "tile", comment=f"by {nx}x{ny}", parents=[self], c="#f28482"
569        )
570        return pic

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, axis='z', preserve_extents=False):
572    def append(self, images, axis="z", preserve_extents=False):
573        """
574        Append the input images to the current one along the specified axis.
575        Except for the append axis, all inputs must have the same extent.
576        All inputs must have the same number of scalar components.
577        The output has the same origin and spacing as the first input.
578        The origin and spacing of all other inputs are ignored.
579        All inputs must have the same scalar type.
580
581        Arguments:
582            axis : (int, str)
583                axis expanded to hold the multiple images
584            preserve_extents : (bool)
585                if True, the extent of the inputs is used to place
586                the image in the output. The whole extent of the output is the union of the input
587                whole extents. Any portion of the output not covered by the inputs is set to zero.
588                The origin and spacing is taken from the first input.
589
590        Example:
591            ```python
592            from vedo import Image, dataurl
593            pic = Image(dataurl+'dog.jpg').pad()
594            pic.append([pic,pic], axis='y')
595            pic.append([pic,pic,pic], axis='x')
596            pic.show(axes=1).close()
597            ```
598            ![](https://vedo.embl.es/images/feats/pict_append.png)
599        """
600        ima = vtk.new("ImageAppend")
601        ima.SetInputData(self.dataset)
602        if not utils.is_sequence(images):
603            images = [images]
604        for p in images:
605            if isinstance(p, vtk.vtkImageData):
606                ima.AddInputData(p)
607            else:
608                ima.AddInputData(p.dataset)
609        ima.SetPreserveExtents(preserve_extents)
610        if axis == "x":
611            axis = 0
612        elif axis == "y":
613            axis = 1
614        ima.SetAppendAxis(axis)
615        ima.Update()
616        self._update(ima.GetOutput())
617        self.pipeline = utils.OperationNode(
618            "append", comment=f"axis={axis}", parents=[self, *images], c="#f28482"
619        )
620        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):
622    def resize(self, newsize):
623        """
624        Resize the image resolution by specifying the number of pixels in width and height.
625        If left to zero, it will be automatically calculated to keep the original aspect ratio.
626
627        `newsize` is the shape of image as [npx, npy], or it can be also expressed as a fraction.
628        """
629        old_dims = np.array(self.dataset.GetDimensions())
630
631        if not utils.is_sequence(newsize):
632            newsize = (old_dims * newsize + 0.5).astype(int)
633
634        if not newsize[1]:
635            ar = old_dims[1] / old_dims[0]
636            newsize = [newsize[0], int(newsize[0] * ar + 0.5)]
637        if not newsize[0]:
638            ar = old_dims[0] / old_dims[1]
639            newsize = [int(newsize[1] * ar + 0.5), newsize[1]]
640        newsize = [newsize[0], newsize[1], old_dims[2]]
641
642        rsz = vtk.new("ImageResize")
643        rsz.SetInputData(self.dataset)
644        rsz.SetResizeMethodToOutputDimensions()
645        rsz.SetOutputDimensions(newsize)
646        rsz.Update()
647        out = rsz.GetOutput()
648        out.SetSpacing(1, 1, 1)
649        self._update(out)
650        self.pipeline = utils.OperationNode(
651            "resize", comment=f"shape={tuple(self.shape)}", parents=[self], c="#f28482"
652        )
653        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'):
655    def mirror(self, axis="x"):
656        """Mirror image along x or y axis. Same as `flip()`."""
657        ff = vtk.new("ImageFlip")
658        ff.SetInputData(self.dataset)
659        if axis.lower() == "x":
660            ff.SetFilteredAxis(0)
661        elif axis.lower() == "y":
662            ff.SetFilteredAxis(1)
663        else:
664            colors.printc("Error in mirror(): mirror must be set to x or y.", c="r")
665            raise RuntimeError()
666        ff.Update()
667        self._update(ff.GetOutput())
668        self.pipeline = utils.OperationNode(f"mirror {axis}", parents=[self], c="#f28482")
669        return self

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

def flip(self, axis='y'):
671    def flip(self, axis="y"):
672        """Mirror image along x or y axis. Same as `mirror()`."""
673        return self.mirror(axis=axis)

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

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

Select one single component of the rgb image.

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

Make it black and white using luminance calibration.

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

Reverse Fast Fourier transform of a image.

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

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

def binarize(self, threshold=None, invert=False):
1051    def binarize(self, threshold=None, invert=False):
1052        """
1053        Return a new Image where pixel above threshold are set to 255
1054        and pixels below are set to 0.
1055
1056        Arguments:
1057            threshold : (float)
1058                input threshold value
1059            invert : (bool)
1060                invert threshold direction
1061
1062        Example:
1063        ```python
1064        from vedo import Image, show
1065        pic1 = Image("https://aws.glamour.es/prod/designs/v1/assets/620x459/547577.jpg")
1066        pic2 = pic1.clone().invert()
1067        pic3 = pic1.clone().binarize()
1068        show(pic1, pic2, pic3, N=3, bg="blue9").close()
1069        ```
1070        ![](https://vedo.embl.es/images/feats/pict_binarize.png)
1071        """
1072        rgb = self.tonumpy()
1073        if rgb.ndim == 3:
1074            intensity = np.sum(rgb, axis=2) / 3
1075        else:
1076            intensity = rgb
1077
1078        if threshold is None:
1079            vmin, vmax = np.min(intensity), np.max(intensity)
1080            threshold = (vmax + vmin) / 2
1081
1082        data = np.zeros_like(intensity).astype(np.uint8)
1083        mask = np.where(intensity > threshold)
1084        if invert:
1085            data += 255
1086            data[mask] = 0
1087        else:
1088            data[mask] = 255
1089
1090        self._update(_get_img(data, flip=True))
1091
1092        self.pipeline = utils.OperationNode(
1093            "binarize", comment=f"threshold={threshold}", parents=[self], c="#f28482"
1094        )
1095        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):
1097    def threshold(self, value=None, flip=False):
1098        """
1099        Create a polygonal Mesh from a Image by filling regions with pixels
1100        luminosity above a specified value.
1101
1102        Arguments:
1103            value : (float)
1104                The default is None, e.i. 1/3 of the scalar range.
1105            flip: (bool)
1106                Flip polygon orientations
1107
1108        Returns:
1109            A polygonal mesh.
1110        """
1111        mgf = vtk.new("ImageMagnitude")
1112        mgf.SetInputData(self.dataset)
1113        mgf.Update()
1114        msq = vtk.new("MarchingSquares")
1115        msq.SetInputData(mgf.GetOutput())
1116        if value is None:
1117            r0, r1 = self.dataset.GetScalarRange()
1118            value = r0 + (r1 - r0) / 3
1119        msq.SetValue(0, value)
1120        msq.Update()
1121        if flip:
1122            rs = vtk.new("ReverseSense")
1123            rs.SetInputData(msq.GetOutput())
1124            rs.ReverseCellsOn()
1125            rs.ReverseNormalsOff()
1126            rs.Update()
1127            output = rs.GetOutput()
1128        else:
1129            output = msq.GetOutput()
1130        ctr = vtk.new("ContourTriangulator")
1131        ctr.SetInputData(output)
1132        ctr.Update()
1133        out = vedo.Mesh(ctr.GetOutput(), c="k").bc("t").lighting("off")
1134
1135        out.pipeline = utils.OperationNode(
1136            "threshold", comment=f"{value: .2f}", parents=[self], c="#f28482:#e9c46a"
1137        )
1138        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, vmin=None, vmax=None):
1140    def cmap(self, name, vmin=None, vmax=None):
1141        """Colorize a image with a colormap representing pixel intensity"""
1142        n = self.dataset.GetPointData().GetNumberOfComponents()
1143        if n > 1:
1144            ecr = vtk.new("ImageExtractComponents")
1145            ecr.SetInputData(self.dataset)
1146            ecr.SetComponents(0, 1, 2)
1147            ecr.Update()
1148            ilum = vtk.new("ImageMagnitude")
1149            ilum.SetInputData(self.dataset)
1150            ilum.Update()
1151            img = ilum.GetOutput()
1152        else:
1153            img = self.dataset
1154
1155        lut = vtk.vtkLookupTable()
1156        _vmin, _vmax = img.GetScalarRange()
1157        if vmin is not None:
1158            _vmin = vmin
1159        if vmax is not None:
1160            _vmax = vmax
1161        lut.SetRange(_vmin, _vmax)
1162
1163        ncols = 256
1164        lut.SetNumberOfTableValues(ncols)
1165        cols = colors.color_map(range(ncols), name, 0, ncols)
1166        for i, c in enumerate(cols):
1167            lut.SetTableValue(i, *c)
1168        lut.Build()
1169
1170        imap = vtk.new("ImageMapToColors")
1171        imap.SetLookupTable(lut)
1172        imap.SetInputData(img)
1173        imap.Update()
1174        self._update(imap.GetOutput())
1175        self.pipeline = utils.OperationNode(
1176            "cmap", comment=f'"{name}"', parents=[self], c="#f28482"
1177        )
1178        return self

Colorize a image with a colormap representing pixel intensity

def rotate(self, angle, center=(), scale=1, mirroring=False, bc='w', alpha=1):
1180    def rotate(self, angle, center=(), scale=1, mirroring=False, bc="w", alpha=1):
1181        """
1182        Rotate by the specified angle (anticlockwise).
1183
1184        Arguments:
1185            angle : (float)
1186                rotation angle in degrees
1187            center : (list)
1188                center of rotation (x,y) in pixels
1189        """
1190        bounds = self.bounds()
1191        pc = [0, 0, 0]
1192        if center:
1193            pc[0] = center[0]
1194            pc[1] = center[1]
1195        else:
1196            pc[0] = (bounds[1] + bounds[0]) / 2.0
1197            pc[1] = (bounds[3] + bounds[2]) / 2.0
1198        pc[2] = (bounds[5] + bounds[4]) / 2.0
1199
1200        transform = vtk.vtkTransform()
1201        transform.Translate(pc)
1202        transform.RotateWXYZ(-angle, 0, 0, 1)
1203        transform.Scale(1 / scale, 1 / scale, 1)
1204        transform.Translate(-pc[0], -pc[1], -pc[2])
1205
1206        reslice = vtk.new("ImageReslice")
1207        reslice.SetMirror(mirroring)
1208        c = np.array(colors.get_color(bc)) * 255
1209        reslice.SetBackgroundColor([c[0], c[1], c[2], alpha * 255])
1210        reslice.SetInputData(self.dataset)
1211        reslice.SetResliceTransform(transform)
1212        reslice.SetOutputDimensionality(2)
1213        reslice.SetInterpolationModeToCubic()
1214        reslice.AutoCropOutputOn()
1215        reslice.Update()
1216        self._update(reslice.GetOutput())
1217
1218        self.pipeline = utils.OperationNode(
1219            "rotate", comment=f"angle={angle}", parents=[self], c="#f28482"
1220        )
1221        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):
1223    def tomesh(self):
1224        """
1225        Convert an image to polygonal data (quads),
1226        with each polygon vertex assigned a RGBA value.
1227        """
1228        dims = self.dataset.GetDimensions()
1229        gr = vedo.shapes.Grid(s=dims[:2], res=(dims[0] - 1, dims[1] - 1))
1230        gr.pos(int(dims[0] / 2), int(dims[1] / 2)).pickable(True).wireframe(False).lw(0)
1231        self.dataset.GetPointData().GetScalars().SetName("RGBA")
1232        gr.dataset.GetPointData().AddArray(self.dataset.GetPointData().GetScalars())
1233        gr.dataset.GetPointData().SetActiveScalars("RGBA")
1234        gr.mapper.SetArrayName("RGBA")
1235        gr.mapper.SetScalarModeToUsePointData()
1236        gr.mapper.ScalarVisibilityOn()
1237        gr.name = self.name
1238        gr.filename = self.filename
1239        gr.pipeline = utils.OperationNode("tomesh", parents=[self], c="#f28482:#e9c46a")
1240        return gr

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

def tonumpy(self):
1242    def tonumpy(self):
1243        """
1244        Get read-write access to pixels of a Image object as a numpy array.
1245        Note that the shape is (nrofchannels, nx, ny).
1246
1247        When you set values in the output image, you don't want numpy to reallocate the array
1248        but instead set values in the existing array, so use the [:] operator.
1249        Example: arr[:] = arr - 15
1250
1251        If the array is modified call:
1252        `image.modified()`
1253        when all your modifications are completed.
1254        """
1255        nx, ny, _ = self.dataset.GetDimensions()
1256        nchan = self.dataset.GetPointData().GetScalars().GetNumberOfComponents()
1257        narray = utils.vtk2numpy(self.dataset.GetPointData().GetScalars()).reshape(ny, nx, nchan)
1258        narray = np.flip(narray, axis=0).astype(np.uint8)
1259        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, yspan, c='green5', alpha=1):
1261    def add_rectangle(self, xspan, yspan, c="green5", alpha=1):
1262        """Draw a rectangle box on top of current image. Units are pixels.
1263
1264        Example:
1265            ```python
1266            import vedo
1267            pic = vedo.Image(vedo.dataurl+"images/dog.jpg")
1268            pic.add_rectangle([100,300], [100,200], c='green4', alpha=0.7)
1269            pic.add_line([100,100],[400,500], lw=2, alpha=1)
1270            pic.add_triangle([250,300], [100,300], [200,400], c='blue5')
1271            vedo.show(pic, axes=1).close()
1272            ```
1273            ![](https://vedo.embl.es/images/feats/pict_drawon.png)
1274        """
1275        x1, x2 = xspan
1276        y1, y2 = yspan
1277
1278        r, g, b = vedo.colors.get_color(c)
1279        c = np.array([r, g, b]) * 255
1280        c = c.astype(np.uint8)
1281
1282        alpha = min(alpha, 1)
1283        if alpha <= 0:
1284            return self
1285        alpha2 = alpha
1286        alpha1 = 1 - alpha
1287
1288        nx, ny = self.dimensions()
1289        if x2 > nx:
1290            x2 = nx - 1
1291        if y2 > ny:
1292            y2 = ny - 1
1293
1294        nchan = self.channels()
1295        narrayA = self.tonumpy()
1296
1297        canvas_source = vtk.new("ImageCanvasSource2D")
1298        canvas_source.SetExtent(0, nx - 1, 0, ny - 1, 0, 0)
1299        canvas_source.SetScalarTypeToUnsignedChar()
1300        canvas_source.SetNumberOfScalarComponents(nchan)
1301        canvas_source.SetDrawColor(255, 255, 255)
1302        canvas_source.FillBox(x1, x2, y1, y2)
1303        canvas_source.Update()
1304        imagedataset = canvas_source.GetOutput()
1305
1306        vscals = imagedataset.GetPointData().GetScalars()
1307        narrayB = vedo.utils.vtk2numpy(vscals).reshape(ny, nx, nchan)
1308        narrayB = np.flip(narrayB, axis=0)
1309        narrayC = np.where(narrayB < 255, narrayA, alpha1 * narrayA + alpha2 * c)
1310        self._update(_get_img(narrayC))
1311        self.pipeline = utils.OperationNode("rectangle", parents=[self], c="#f28482")
1312        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, p2, lw=2, c='k2', alpha=1):
1314    def add_line(self