vedo.image

Submodule to work with common format images.

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

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

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

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

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

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

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

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

shape: numpy.ndarray
379    @property
380    def shape(self) -> np.ndarray:
381        """Return the image shape as number of pixels in x and y"""
382        return np.array(self.dimensions())

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

channels: int
384    @property
385    def channels(self) -> int:
386        """Return the number of channels in image"""
387        return self.dataset.GetPointData().GetScalars().GetNumberOfComponents()

Return the number of channels in image

extent: Tuple[int, int, int, int]
389    @property
390    def extent(self) -> Tuple[int, int, int, int]:
391        """Return the physical extent that the image spans."""
392        return self.dataset.GetExtent()

Return the physical extent that the image spans.

def copy(self) -> Image:
400    def copy(self) -> "Image":
401        """Return a copy of the image. Alias of `clone()`."""
402        return self.clone()

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

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

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

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

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

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

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

Crop image.

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

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

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

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

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

Append the input images to the current one along the specified axis. Except for the append axis, all inputs must have the same extent. All inputs must have the same number of scalar components. The output has the same origin and spacing as the first input. The origin and spacing of all other inputs are ignored. All inputs must have the same scalar type.

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

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

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

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

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

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

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

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

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

Select one single component of the rgb image.

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

Make it black and white using luminance calibration.

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

Smooth a Image with Gaussian kernel.

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

Median filter that preserves thin lines and corners.

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

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

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

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

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

Fast Fourier transform of a image.

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

Reverse Fast Fourier transform of a image.

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

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

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

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

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

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

Warp an image using thin-plate splines.

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

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

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

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

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

Example:

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

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

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

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

A polygonal mesh.

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

Colorize a image with a colormap representing pixel intensity

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

Rotate by the specified angle (anticlockwise).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Add text to an image.

def modified(self) -> Self:
1475    def modified(self) -> Self:
1476        """Use this method in conjunction with `tonumpy()`
1477        to update any modifications to the image array."""
1478        self.dataset.GetPointData().GetScalars().Modified()
1479        return self

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

def write(self, filename: str) -> Self:
1481    def write(self, filename: str) -> Self:
1482        """Write image to file as png or jpg."""
1483        filename = str(filename)
1484        vedo.file_io.write(self, filename)
1485        self.pipeline = utils.OperationNode(
1486            "write",
1487            comment=filename[:15],
1488            parents=[self],
1489            c="#8a817c",
1490            shape="cylinder",
1491        )
1492        return self

Write image to file as png or jpg.