vedo.picture

Submodule to work with common format images.

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

Derived class of vtkImageActor. Used to represent 2D pictures in a 3D world.

Picture(obj=None, channels=3, flip=False)
250    def __init__(self, obj=None, channels=3, flip=False):
251        """
252        Can be instantiated with a path file name or with a numpy array.
253
254        By default the transparency channel is disabled.
255        To enable it set channels=4.
256
257        Use `Picture.dimensions()` to access the number of pixels in x and y.
258
259        Arguments:
260            channels :  (int, list)
261                only select these specific rgba channels (useful to remove alpha)
262            flip : (bool)
263                flip xy axis convention (when input is a numpy array)
264        """
265
266        vtk.vtkImageActor.__init__(self)
267        vedo.base.Base3DProp.__init__(self)
268
269        if utils.is_sequence(obj) and len(obj) > 0:  # passing array
270            img = _get_img(obj, flip)
271
272        elif isinstance(obj, vtk.vtkImageData):
273            img = obj
274
275        elif isinstance(obj, str):
276            img = _get_img(obj)
277            self.filename = obj
278
279        else:
280            img = vtk.vtkImageData()
281
282        # select channels
283        if isinstance(channels, int):
284            channels = list(range(channels))
285
286        nchans = len(channels)
287        n = img.GetPointData().GetScalars().GetNumberOfComponents()
288        if nchans and n > nchans:
289            pec = vtk.vtkImageExtractComponents()
290            pec.SetInputData(img)
291            if nchans == 4:
292                pec.SetComponents(channels[0], channels[1], channels[2], channels[3])
293            elif nchans == 3:
294                pec.SetComponents(channels[0], channels[1], channels[2])
295            elif nchans == 2:
296                pec.SetComponents(channels[0], channels[1])
297            elif nchans == 1:
298                pec.SetComponents(channels[0])
299            pec.Update()
300            img = pec.GetOutput()
301
302        self._data = img
303        self.SetInputData(img)
304
305        sx, sy, _ = img.GetDimensions()
306        self.shape = np.array([sx, sy])
307
308        self._mapper = self.GetMapper()
309
310        self.pipeline = utils.OperationNode("Picture", comment=f"#shape {self.shape}", c="#f28482")
311        ######################################################################

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

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

Use Picture.dimensions() to access the number of pixels in x and y.

Arguments:
  • channels : (int, list) only select these specific rgba channels (useful to remove alpha)
  • flip : (bool) flip xy axis convention (when input is a numpy array)
def inputdata(self):
386    def inputdata(self):
387        """Return the underlying ``vtkImagaData`` object."""
388        return self._data

Return the underlying vtkImagaData object.

def dimensions(self):
390    def dimensions(self):
391        """Return the picture dimension as number of pixels in x and y"""
392        nx, ny, _ = self._data.GetDimensions()
393        return np.array([nx, ny])

Return the picture dimension as number of pixels in x and y

def channels(self):
395    def channels(self):
396        """Return the number of channels in picture"""
397        return self._data.GetPointData().GetScalars().GetNumberOfComponents()

Return the number of channels in picture

def clone(self, transformed=False):
408    def clone(self, transformed=False):
409        """Return an exact copy of the input Picture.
410        If transform is True, it is given the same scaling and position."""
411        img = vtk.vtkImageData()
412        img.DeepCopy(self._data)
413        pic = Picture(img)
414        if transformed:
415            # assign the same transformation to the copy
416            pic.SetOrigin(self.GetOrigin())
417            pic.SetScale(self.GetScale())
418            pic.SetOrientation(self.GetOrientation())
419            pic.SetPosition(self.GetPosition())
420
421        pic.pipeline = utils.OperationNode("clone", parents=[self], c="#f7dada", shape="diamond")
422        return pic

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

def cmap(self, name, vmin=None, vmax=None):
424    def cmap(self, name, vmin=None, vmax=None):
425        """Colorize a picture with a colormap representing pixel intensity"""
426        n = self._data.GetPointData().GetNumberOfComponents()
427        if n > 1:
428            ecr = vtk.vtkImageExtractComponents()
429            ecr.SetInputData(self._data)
430            ecr.SetComponents(0, 1, 2)
431            ecr.Update()
432            ilum = vtk.vtkImageMagnitude()
433            ilum.SetInputData(self._data)
434            ilum.Update()
435            img = ilum.GetOutput()
436        else:
437            img = self._data
438
439        lut = vtk.vtkLookupTable()
440        _vmin, _vmax = img.GetScalarRange()
441        if vmin is not None:
442            _vmin = vmin
443        if vmax is not None:
444            _vmax = vmax
445        lut.SetRange(_vmin, _vmax)
446
447        ncols = 256
448        lut.SetNumberOfTableValues(ncols)
449        cols = colors.color_map(range(ncols), name, 0, ncols)
450        for i, c in enumerate(cols):
451            lut.SetTableValue(i, *c)
452        lut.Build()
453
454        imap = vtk.vtkImageMapToColors()
455        imap.SetLookupTable(lut)
456        imap.SetInputData(img)
457        imap.Update()
458        self._update(imap.GetOutput())
459        self.pipeline = utils.OperationNode(
460            f"cmap", comment=f'"{name}"', parents=[self], c="#f28482"
461        )
462        return self

Colorize a picture with a colormap representing pixel intensity

def extent(self, ext=None):
464    def extent(self, ext=None):
465        """
466        Get or set the physical extent that the picture spans.
467        Format is `ext=[minx, maxx, miny, maxy]`.
468        """
469        if ext is None:
470            return self._data.GetExtent()
471
472        self._data.SetExtent(ext[0], ext[1], ext[2], ext[3], 0, 0)
473        self._mapper.Modified()
474        return self

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

def alpha(self, a=None):
476    def alpha(self, a=None):
477        """Set/get picture's transparency in the rendering scene."""
478        if a is not None:
479            self.GetProperty().SetOpacity(a)
480            return self
481        return self.GetProperty().GetOpacity()

Set/get picture's transparency in the rendering scene.

def level(self, value=None):
483    def level(self, value=None):
484        """Get/Set the image color level (brightness) in the rendering scene."""
485        if value is None:
486            return self.GetProperty().GetColorLevel()
487        self.GetProperty().SetColorLevel(value)
488        return self

Get/Set the image color level (brightness) in the rendering scene.

def window(self, value=None):
490    def window(self, value=None):
491        """Get/Set the image color window (contrast) in the rendering scene."""
492        if value is None:
493            return self.GetProperty().GetColorWindow()
494        self.GetProperty().SetColorWindow(value)
495        return self

Get/Set the image color window (contrast) in the rendering scene.

def crop(self, top=None, bottom=None, right=None, left=None, pixels=False):
497    def crop(self, top=None, bottom=None, right=None, left=None, pixels=False):
498        """Crop picture.
499
500        Arguments:
501            top : (float)
502                fraction to crop from the top margin
503            bottom : (float)
504                fraction to crop from the bottom margin
505            left : (float)
506                fraction to crop from the left margin
507            right : (float)
508                fraction to crop from the right margin
509            pixels : (bool)
510                units are pixels
511        """
512        extractVOI = vtk.vtkExtractVOI()
513        extractVOI.SetInputData(self._data)
514        extractVOI.IncludeBoundaryOn()
515
516        d = self.GetInput().GetDimensions()
517        if pixels:
518            extractVOI.SetVOI(left, d[0] - right - 1, bottom, d[1] - top - 1, 0, 0)
519        else:
520            bx0, bx1, by0, by1 = 0, d[0]-1, 0, d[1]-1
521            if left is not None:   bx0 = int((d[0]-1)*left)
522            if right is not None:  bx1 = int((d[0]-1)*(1-right))
523            if bottom is not None: by0 = int((d[1]-1)*bottom)
524            if top is not None:    by1 = int((d[1]-1)*(1-top))
525            extractVOI.SetVOI(bx0, bx1, by0, by1, 0, 0)
526        extractVOI.Update()
527
528        self.shape = extractVOI.GetOutput().GetDimensions()[:2]
529        self._update(extractVOI.GetOutput())
530        self.pipeline = utils.OperationNode(
531            "crop", comment=f"shape={tuple(self.shape)}", parents=[self], c="#f28482"
532        )
533        return self

Crop picture.

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):
535    def pad(self, pixels=10, value=255):
536        """
537        Add the specified number of pixels at the picture borders.
538        Pixels can be a list formatted as [left,right,bottom,top].
539
540        Arguments:
541            pixels : (int, list)
542                number of pixels to be added (or a list of length 4)
543            value : (int)
544                intensity value (gray-scale color) of the padding
545        """
546        x0, x1, y0, y1, _z0, _z1 = self._data.GetExtent()
547        pf = vtk.vtkImageConstantPad()
548        pf.SetInputData(self._data)
549        pf.SetConstant(value)
550        if utils.is_sequence(pixels):
551            pf.SetOutputWholeExtent(
552                x0 - pixels[0], x1 + pixels[1], 
553                y0 - pixels[2], y1 + pixels[3], 
554                0, 0
555            )
556        else:
557            pf.SetOutputWholeExtent(
558                x0 - pixels, x1 + pixels, 
559                y0 - pixels, y1 + pixels, 
560                0, 0
561            )
562        pf.Update()
563        self._update(pf.GetOutput())
564        self.pipeline = utils.OperationNode(
565            "pad", comment=f"{pixels} pixels", parents=[self], c="#f28482"
566        )
567        return self

Add the specified number of pixels at the picture 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)):
569    def tile(self, nx=4, ny=4, shift=(0, 0)):
570        """
571        Generate a tiling from the current picture by mirroring and repeating it.
572
573        Arguments:
574            nx : (float)
575                number of repeats along x
576            ny : (float)
577                number of repeats along x
578            shift : (list)
579                shift in x and y in pixels
580        """
581        x0, x1, y0, y1, z0, z1 = self._data.GetExtent()
582        constant_pad = vtk.vtkImageMirrorPad()
583        constant_pad.SetInputData(self._data)
584        constant_pad.SetOutputWholeExtent(
585            int(x0 + shift[0] + 0.5),
586            int(x1 * nx + shift[0] + 0.5),
587            int(y0 + shift[1] + 0.5),
588            int(y1 * ny + shift[1] + 0.5),
589            z0,
590            z1,
591        )
592        constant_pad.Update()
593        pic = Picture(constant_pad.GetOutput())
594
595        pic.pipeline = utils.OperationNode(
596            "tile", comment=f"by {nx}x{ny}", parents=[self], c="#f28482"
597        )
598        return pic

Generate a tiling from the current picture 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, pictures, axis='z', preserve_extents=False):
600    def append(self, pictures, axis="z", preserve_extents=False):
601        """
602        Append the input images to the current one along the specified axis.
603        Except for the append axis, all inputs must have the same extent.
604        All inputs must have the same number of scalar components.
605        The output has the same origin and spacing as the first input.
606        The origin and spacing of all other inputs are ignored.
607        All inputs must have the same scalar type.
608
609        Arguments:
610            axis : (int, str)
611                axis expanded to hold the multiple images
612            preserve_extents : (bool)
613                if True, the extent of the inputs is used to place
614                the image in the output. The whole extent of the output is the union of the input
615                whole extents. Any portion of the output not covered by the inputs is set to zero.
616                The origin and spacing is taken from the first input.
617
618        Example:
619            ```python
620            from vedo import Picture, dataurl
621            pic = Picture(dataurl+'dog.jpg').pad()
622            pic.append([pic,pic], axis='y')
623            pic.append([pic,pic,pic], axis='x')
624            pic.show(axes=1).close()
625            ```
626            ![](https://vedo.embl.es/images/feats/pict_append.png)
627        """
628        ima = vtk.vtkImageAppend()
629        ima.SetInputData(self._data)
630        if not utils.is_sequence(pictures):
631            pictures = [pictures]
632        for p in pictures:
633            if isinstance(p, vtk.vtkImageData):
634                ima.AddInputData(p)
635            else:
636                ima.AddInputData(p.inputdata())
637        ima.SetPreserveExtents(preserve_extents)
638        if axis == "x":
639            axis = 0
640        elif axis == "y":
641            axis = 1
642        ima.SetAppendAxis(axis)
643        ima.Update()
644        self._update(ima.GetOutput())
645        self.pipeline = utils.OperationNode(
646            "append", comment=f"axis={axis}", parents=[self, *pictures], c="#f28482"
647        )
648        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 Picture, dataurl
pic = Picture(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):
650    def resize(self, newsize):
651        """Resize the image resolution by specifying the number of pixels in width and height.
652        If left to zero, it will be automatically calculated to keep the original aspect ratio.
653
654        newsize is the shape of picture as [npx, npy], or it can be also expressed as a fraction.
655        """
656        old_dims = np.array(self._data.GetDimensions())
657
658        if not utils.is_sequence(newsize):
659            newsize = (old_dims * newsize + 0.5).astype(int)
660
661        if not newsize[1]:
662            ar = old_dims[1] / old_dims[0]
663            newsize = [newsize[0], int(newsize[0] * ar + 0.5)]
664        if not newsize[0]:
665            ar = old_dims[0] / old_dims[1]
666            newsize = [int(newsize[1] * ar + 0.5), newsize[1]]
667        newsize = [newsize[0], newsize[1], old_dims[2]]
668
669        rsz = vtk.vtkImageResize()
670        rsz.SetInputData(self._data)
671        rsz.SetResizeMethodToOutputDimensions()
672        rsz.SetOutputDimensions(newsize)
673        rsz.Update()
674        out = rsz.GetOutput()
675        out.SetSpacing(1, 1, 1)
676        self._update(out)
677        self.pipeline = utils.OperationNode(
678            "resize", comment=f"shape={tuple(self.shape)}", parents=[self], c="#f28482"
679        )
680        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 picture as [npx, npy], or it can be also expressed as a fraction.

def mirror(self, axis='x'):
682    def mirror(self, axis="x"):
683        """Mirror picture along x or y axis. Same as `flip()`."""
684        ff = vtk.vtkImageFlip()
685        ff.SetInputData(self.inputdata())
686        if axis.lower() == "x":
687            ff.SetFilteredAxis(0)
688        elif axis.lower() == "y":
689            ff.SetFilteredAxis(1)
690        else:
691            colors.printc("Error in mirror(): mirror must be set to x or y.", c="r")
692            raise RuntimeError()
693        ff.Update()
694        self._update(ff.GetOutput())
695        self.pipeline = utils.OperationNode(f"mirror {axis}", parents=[self], c="#f28482")
696        return self

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

def flip(self, axis='y'):
698    def flip(self, axis="y"):
699        """Mirror picture along x or y axis. Same as `mirror()`."""
700        return self.mirror(axis=axis)

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

def rotate(self, angle, center=(), scale=1, mirroring=False, bc='w', alpha=1):
702    def rotate(self, angle, center=(), scale=1, mirroring=False, bc="w", alpha=1):
703        """
704        Rotate by the specified angle (anticlockwise).
705
706        Arguments:
707            angle : (float)
708                rotation angle in degrees
709            center : (list)
710                center of rotation (x,y) in pixels
711        """
712        bounds = self.bounds()
713        pc = [0, 0, 0]
714        if center:
715            pc[0] = center[0]
716            pc[1] = center[1]
717        else:
718            pc[0] = (bounds[1] + bounds[0]) / 2.0
719            pc[1] = (bounds[3] + bounds[2]) / 2.0
720        pc[2] = (bounds[5] + bounds[4]) / 2.0
721
722        transform = vtk.vtkTransform()
723        transform.Translate(pc)
724        transform.RotateWXYZ(-angle, 0, 0, 1)
725        transform.Scale(1 / scale, 1 / scale, 1)
726        transform.Translate(-pc[0], -pc[1], -pc[2])
727
728        reslice = vtk.vtkImageReslice()
729        reslice.SetMirror(mirroring)
730        c = np.array(colors.get_color(bc)) * 255
731        reslice.SetBackgroundColor([c[0], c[1], c[2], alpha * 255])
732        reslice.SetInputData(self._data)
733        reslice.SetResliceTransform(transform)
734        reslice.SetOutputDimensionality(2)
735        reslice.SetInterpolationModeToCubic()
736        reslice.SetOutputSpacing(self._data.GetSpacing())
737        reslice.SetOutputOrigin(self._data.GetOrigin())
738        reslice.SetOutputExtent(self._data.GetExtent())
739        reslice.Update()
740        self._update(reslice.GetOutput())
741
742        self.pipeline = utils.OperationNode(
743            "rotate", comment=f"angle={angle}", parents=[self], c="#f28482"
744        )
745        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 select(self, component):
747    def select(self, component):
748        """Select one single component of the rgb image."""
749        ec = vtk.vtkImageExtractComponents()
750        ec.SetInputData(self._data)
751        ec.SetComponents(component)
752        ec.Update()
753        pic = Picture(ec.GetOutput())
754        pic.pipeline = utils.OperationNode(
755            "select", comment=f"component {component}", parents=[self], c="#f28482"
756        )
757        return pic

Select one single component of the rgb image.

def bw(self):
759    def bw(self):
760        """Make it black and white using luminance calibration."""
761        n = self._data.GetPointData().GetNumberOfComponents()
762        if n == 4:
763            ecr = vtk.vtkImageExtractComponents()
764            ecr.SetInputData(self._data)
765            ecr.SetComponents(0, 1, 2)
766            ecr.Update()
767            img = ecr.GetOutput()
768        else:
769            img = self._data
770
771        ecr = vtk.vtkImageLuminance()
772        ecr.SetInputData(img)
773        ecr.Update()
774        self._update(ecr.GetOutput())
775        self.pipeline = utils.OperationNode("black&white", parents=[self], c="#f28482")
776        return self

Make it black and white using luminance calibration.

def smooth(self, sigma=3, radius=None):
778    def smooth(self, sigma=3, radius=None):
779        """
780        Smooth a Picture with Gaussian kernel.
781
782        Arguments:
783            sigma : (int)
784                number of sigmas in pixel units
785            radius : (float)
786                how far out the gaussian kernel will go before being clamped to zero
787        """
788        gsf = vtk.vtkImageGaussianSmooth()
789        gsf.SetDimensionality(2)
790        gsf.SetInputData(self._data)
791        if radius is not None:
792            if utils.is_sequence(radius):
793                gsf.SetRadiusFactors(radius[0], radius[1])
794            else:
795                gsf.SetRadiusFactor(radius)
796
797        if utils.is_sequence(sigma):
798            gsf.SetStandardDeviations(sigma[0], sigma[1])
799        else:
800            gsf.SetStandardDeviation(sigma)
801        gsf.Update()
802        self._update(gsf.GetOutput())
803        self.pipeline = utils.OperationNode(
804            "smooth", comment=f"sigma={sigma}", parents=[self], c="#f28482"
805        )
806        return self

Smooth a Picture 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):
808    def median(self):
809        """
810        Median filter that preserves thin lines and corners.
811
812        It operates on a 5x5 pixel neighborhood. It computes two values initially:
813        the median of the + neighbors and the median of the x neighbors.
814        It then computes the median of these two values plus the center pixel.
815        This result of this second median is the output pixel value.
816        """
817        medf = vtk.vtkImageHybridMedian2D()
818        medf.SetInputData(self._data)
819        medf.Update()
820        self._update(medf.GetOutput())
821        self.pipeline = utils.OperationNode("median", parents=[self], c="#f28482")
822        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):
824    def enhance(self):
825        """
826        Enhance a b&w picture using the laplacian, enhancing high-freq edges.
827
828        Example:
829            ```python
830            from vedo import *
831            pic = Picture(vedo.dataurl+'images/dog.jpg').bw()
832            show(pic, pic.clone().enhance(), N=2, mode='image', zoom='tight')
833            ```
834            ![](https://vedo.embl.es/images/feats/pict_enhance.png)
835        """
836        img = self._data
837        scalarRange = img.GetPointData().GetScalars().GetRange()
838
839        cast = vtk.vtkImageCast()
840        cast.SetInputData(img)
841        cast.SetOutputScalarTypeToDouble()
842        cast.Update()
843
844        laplacian = vtk.vtkImageLaplacian()
845        laplacian.SetInputData(cast.GetOutput())
846        laplacian.SetDimensionality(2)
847        laplacian.Update()
848
849        subtr = vtk.vtkImageMathematics()
850        subtr.SetInputData(0, cast.GetOutput())
851        subtr.SetInputData(1, laplacian.GetOutput())
852        subtr.SetOperationToSubtract()
853        subtr.Update()
854
855        color_window = scalarRange[1] - scalarRange[0]
856        color_level = color_window / 2
857        original_color = vtk.vtkImageMapToWindowLevelColors()
858        original_color.SetWindow(color_window)
859        original_color.SetLevel(color_level)
860        original_color.SetInputData(subtr.GetOutput())
861        original_color.Update()
862        self._update(original_color.GetOutput())
863
864        self.pipeline = utils.OperationNode("enhance", parents=[self], c="#f28482")
865        return self

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

Example:
from vedo import *
pic = Picture(vedo.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):
867    def fft(self, mode="magnitude", logscale=12, center=True):
868        """
869        Fast Fourier transform of a picture.
870
871        Arguments:
872            logscale : (float)
873                if non-zero, take the logarithm of the intensity and scale it by this factor.
874            mode : (str)
875                either [magnitude, real, imaginary, complex], compute the point array data accordingly.
876            center : (bool)
877                shift constant zero-frequency to the center of the image for display.
878                (FFT converts spatial images into frequency space, but puts the zero frequency at the origin)
879        """
880        ffti = vtk.vtkImageFFT()
881        ffti.SetInputData(self._data)
882        ffti.Update()
883
884        if "mag" in mode:
885            mag = vtk.vtkImageMagnitude()
886            mag.SetInputData(ffti.GetOutput())
887            mag.Update()
888            out = mag.GetOutput()
889        elif "real" in mode:
890            erf = vtk.vtkImageExtractComponents()
891            erf.SetInputData(ffti.GetOutput())
892            erf.SetComponents(0)
893            erf.Update()
894            out = erf.GetOutput()
895        elif "imaginary" in mode:
896            eimf = vtk.vtkImageExtractComponents()
897            eimf.SetInputData(ffti.GetOutput())
898            eimf.SetComponents(1)
899            eimf.Update()
900            out = eimf.GetOutput()
901        elif "complex" in mode:
902            out = ffti.GetOutput()
903        else:
904            colors.printc("Error in fft(): unknown mode", mode)
905            raise RuntimeError()
906
907        if center:
908            center = vtk.vtkImageFourierCenter()
909            center.SetInputData(out)
910            center.Update()
911            out = center.GetOutput()
912
913        if "complex" not in mode:
914            if logscale:
915                ils = vtk.vtkImageLogarithmicScale()
916                ils.SetInputData(out)
917                ils.SetConstant(logscale)
918                ils.Update()
919                out = ils.GetOutput()
920
921        pic = Picture(out)
922        pic.pipeline = utils.OperationNode("FFT", parents=[self], c="#f28482")
923        return pic

Fast Fourier transform of a picture.

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'):
925    def rfft(self, mode="magnitude"):
926        """Reverse Fast Fourier transform of a picture."""
927
928        ffti = vtk.vtkImageRFFT()
929        ffti.SetInputData(self._data)
930        ffti.Update()
931
932        if "mag" in mode:
933            mag = vtk.vtkImageMagnitude()
934            mag.SetInputData(ffti.GetOutput())
935            mag.Update()
936            out = mag.GetOutput()
937        elif "real" in mode:
938            erf = vtk.vtkImageExtractComponents()
939            erf.SetInputData(ffti.GetOutput())
940            erf.SetComponents(0)
941            erf.Update()
942            out = erf.GetOutput()
943        elif "imaginary" in mode:
944            eimf = vtk.vtkImageExtractComponents()
945            eimf.SetInputData(ffti.GetOutput())
946            eimf.SetComponents(1)
947            eimf.Update()
948            out = eimf.GetOutput()
949        elif "complex" in mode:
950            out = ffti.GetOutput()
951        else:
952            colors.printc("Error in rfft(): unknown mode", mode)
953            raise RuntimeError()
954
955        pic = Picture(out)
956        pic.pipeline = utils.OperationNode("rFFT", parents=[self], c="#f28482")
957        return pic

Reverse Fast Fourier transform of a picture.

def filterpass(self, lowcutoff=None, highcutoff=None, order=3):
 959    def filterpass(self, lowcutoff=None, highcutoff=None, order=3):
 960        """
 961        Low-pass and high-pass filtering become trivial in the frequency domain.
 962        A portion of the pixels/voxels are simply masked or attenuated.
 963        This function applies a high pass Butterworth filter that attenuates the
 964        frequency domain image with the function
 965
 966        The gradual attenuation of the filter is important.
 967        A simple high-pass filter would simply mask a set of pixels in the frequency domain,
 968        but the abrupt transition would cause a ringing effect in the spatial domain.
 969
 970        Arguments:
 971            lowcutoff : (list)
 972                the cutoff frequencies
 973            highcutoff : (list)
 974                the cutoff frequencies
 975            order : (int)
 976                order determines sharpness of the cutoff curve
 977        """
 978        # https://lorensen.github.io/VTKExamples/site/Cxx/ImageProcessing/IdealHighPass
 979        fft = vtk.vtkImageFFT()
 980        fft.SetInputData(self._data)
 981        fft.Update()
 982        out = fft.GetOutput()
 983
 984        if highcutoff:
 985            blp = vtk.vtkImageButterworthLowPass()
 986            blp.SetInputData(out)
 987            blp.SetCutOff(highcutoff)
 988            blp.SetOrder(order)
 989            blp.Update()
 990            out = blp.GetOutput()
 991
 992        if lowcutoff:
 993            bhp = vtk.vtkImageButterworthHighPass()
 994            bhp.SetInputData(out)
 995            bhp.SetCutOff(lowcutoff)
 996            bhp.SetOrder(order)
 997            bhp.Update()
 998            out = bhp.GetOutput()
 999
1000        rfft = vtk.vtkImageRFFT()
1001        rfft.SetInputData(out)
1002        rfft.Update()
1003
1004        ecomp = vtk.vtkImageExtractComponents()
1005        ecomp.SetInputData(rfft.GetOutput())
1006        ecomp.SetComponents(0)
1007        ecomp.Update()
1008
1009        caster = vtk.vtkImageCast()
1010        caster.SetOutputScalarTypeToUnsignedChar()
1011        caster.SetInputData(ecomp.GetOutput())
1012        caster.Update()
1013        self._update(caster.GetOutput())
1014        self.pipeline = utils.OperationNode("filterpass", parents=[self], c="#f28482")
1015        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):
1017    def blend(self, pic, alpha1=0.5, alpha2=0.5):
1018        """
1019        Take L, LA, RGB, or RGBA images as input and blends
1020        them according to the alpha values and/or the opacity setting for each input.
1021        """
1022        blf = vtk.vtkImageBlend()
1023        blf.AddInputData(self._data)
1024        blf.AddInputData(pic.inputdata())
1025        blf.SetOpacity(0, alpha1)
1026        blf.SetOpacity(1, alpha2)
1027        blf.SetBlendModeToNormal()
1028        blf.Update()
1029        self._update(blf.GetOutput())
1030        self.pipeline = utils.OperationNode("blend", parents=[self, pic], c="#f28482")
1031        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):
1033    def warp(
1034        self,
1035        source_pts=(),
1036        target_pts=(),
1037        transform=None,
1038        sigma=1,
1039        mirroring=False,
1040        bc="w",
1041        alpha=1,
1042    ):
1043        """
1044        Warp an image using thin-plate splines.
1045
1046        Arguments:
1047            source_pts : (list)
1048                source points
1049            target_pts : (list)
1050                target points
1051            transform : (vtkTransform)
1052                a vtkTransform object can be supplied
1053            sigma : (float), optional
1054                stiffness of the interpolation
1055            mirroring : (bool)
1056                fill the margins with a reflection of the original image
1057            bc : (color)
1058                fill the margins with a solid color
1059            alpha : (float)
1060                opacity of the filled margins
1061        """
1062        if transform is None:
1063            # source and target must be filled
1064            transform = vtk.vtkThinPlateSplineTransform()
1065            transform.SetBasisToR2LogR()
1066
1067            parents = [self]
1068            if isinstance(source_pts, vedo.Points):
1069                parents.append(source_pts)
1070                source_pts = source_pts.points()
1071            if isinstance(target_pts, vedo.Points):
1072                parents.append(target_pts)
1073                target_pts = target_pts.points()
1074
1075            ns = len(source_pts)
1076            nt = len(target_pts)
1077            if ns != nt:
1078                colors.printc("Error in picture.warp(): #source != #target points", ns, nt, c="r")
1079                raise RuntimeError()
1080
1081            ptsou = vtk.vtkPoints()
1082            ptsou.SetNumberOfPoints(ns)
1083
1084            pttar = vtk.vtkPoints()
1085            pttar.SetNumberOfPoints(nt)
1086
1087            for i in range(ns):
1088                p = source_pts[i]
1089                ptsou.SetPoint(i, [p[0], p[1], 0])
1090                p = target_pts[i]
1091                pttar.SetPoint(i, [p[0], p[1], 0])
1092
1093            transform.SetSigma(sigma)
1094            transform.SetSourceLandmarks(pttar)
1095            transform.SetTargetLandmarks(ptsou)
1096        else:
1097            # ignore source and target
1098            pass
1099
1100        reslice = vtk.vtkImageReslice()
1101        reslice.SetInputData(self._data)
1102        reslice.SetOutputDimensionality(2)
1103        reslice.SetResliceTransform(transform)
1104        reslice.SetInterpolationModeToCubic()
1105        reslice.SetMirror(mirroring)
1106        c = np.array(colors.get_color(bc)) * 255
1107        reslice.SetBackgroundColor([c[0], c[1], c[2], alpha * 255])
1108        reslice.Update()
1109        self.transform = transform
1110        self._update(reslice.GetOutput())
1111        self.pipeline = utils.OperationNode("warp", parents=parents, c="#f28482")
1112        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):
1114    def invert(self):
1115        """
1116        Return an inverted picture (inverted in each color channel).
1117        """
1118        rgb = self.tonumpy()
1119        data = 255 - np.array(rgb)
1120        self._update(_get_img(data))
1121        self.pipeline = utils.OperationNode("invert", parents=[self], c="#f28482")
1122        return self

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

def binarize(self, threshold=None, invert=False):
1124    def binarize(self, threshold=None, invert=False):
1125        """
1126        Return a new Picture where pixel above threshold are set to 255
1127        and pixels below are set to 0.
1128
1129        Arguments:
1130            threshold : (float)
1131                input threshold value
1132            invert : (bool)
1133                invert threshold direction
1134
1135        Example:
1136            ```python
1137            from vedo import Picture, show
1138            pic1 = Picture("https://aws.glamour.es/prod/designs/v1/assets/620x459/547577.jpg")
1139            pic2 = pic1.clone().invert()
1140            pic3 = pic1.clone().binarize()
1141            show(pic1, pic2, pic3, N=3, bg="blue9").close()
1142            ```
1143            ![](https://vedo.embl.es/images/feats/pict_binarize.png)
1144        """
1145        rgb = self.tonumpy()
1146        if rgb.ndim == 3:
1147            intensity = np.sum(rgb, axis=2) / 3
1148        else:
1149            intensity = rgb
1150
1151        if threshold is None:
1152            vmin, vmax = np.min(intensity), np.max(intensity)
1153            threshold = (vmax + vmin) / 2
1154
1155        data = np.zeros_like(intensity).astype(np.uint8)
1156        mask = np.where(intensity > threshold)
1157        if invert:
1158            data += 255
1159            data[mask] = 0
1160        else:
1161            data[mask] = 255
1162
1163        self._update(_get_img(data, flip=True))
1164
1165        self.pipeline = utils.OperationNode(
1166            "binarize", comment=f"threshold={threshold}", parents=[self], c="#f28482"
1167        )
1168        return self

Return a new Picture 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 Picture, show
pic1 = Picture("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):
1170    def threshold(self, value=None, flip=False):
1171        """
1172        Create a polygonal Mesh from a Picture by filling regions with pixels
1173        luminosity above a specified value.
1174
1175        Arguments:
1176            value : (float)
1177                The default is None, e.i. 1/3 of the scalar range.
1178            flip: (bool)
1179                Flip polygon orientations
1180
1181        Returns:
1182            A polygonal mesh.
1183        """
1184        mgf = vtk.vtkImageMagnitude()
1185        mgf.SetInputData(self._data)
1186        mgf.Update()
1187        msq = vtk.vtkMarchingSquares()
1188        msq.SetInputData(mgf.GetOutput())
1189        if value is None:
1190            r0, r1 = self._data.GetScalarRange()
1191            value = r0 + (r1 - r0) / 3
1192        msq.SetValue(0, value)
1193        msq.Update()
1194        if flip:
1195            rs = vtk.vtkReverseSense()
1196            rs.SetInputData(msq.GetOutput())
1197            rs.ReverseCellsOn()
1198            rs.ReverseNormalsOff()
1199            rs.Update()
1200            output = rs.GetOutput()
1201        else:
1202            output = msq.GetOutput()
1203        ctr = vtk.vtkContourTriangulator()
1204        ctr.SetInputData(output)
1205        ctr.Update()
1206        out = vedo.Mesh(ctr.GetOutput(), c="k").bc("t").lighting("off")
1207
1208        out.pipeline = utils.OperationNode(
1209            "threshold", comment=f"{value: .2f}", parents=[self], c="#f28482:#e9c46a"
1210        )
1211        return out

Create a polygonal Mesh from a Picture 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 tomesh(self):
1213    def tomesh(self):
1214        """
1215        Convert an image to polygonal data (quads),
1216        with each polygon vertex assigned a RGBA value.
1217        """
1218        dims = self._data.GetDimensions()
1219        gr = vedo.shapes.Grid(s=dims[:2], res=(dims[0] - 1, dims[1] - 1))
1220        gr.pos(int(dims[0] / 2), int(dims[1] / 2)).pickable(True).wireframe(False).lw(0)
1221        self._data.GetPointData().GetScalars().SetName("RGBA")
1222        gr.inputdata().GetPointData().AddArray(self._data.GetPointData().GetScalars())
1223        gr.inputdata().GetPointData().SetActiveScalars("RGBA")
1224        gr.mapper().SetArrayName("RGBA")
1225        gr.mapper().SetScalarModeToUsePointData()
1226        gr.mapper().ScalarVisibilityOn()
1227        gr.name = self.name
1228        gr.filename = self.filename
1229        gr.pipeline = utils.OperationNode("tomesh", parents=[self], c="#f28482:#e9c46a")
1230        return gr

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

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

Get read-write access to pixels of a Picture 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: picture.modified() when all your modifications are completed.

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

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

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

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

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

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

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

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

Add text to an image.

def modified(self):
1463    def modified(self):
1464        """Use in conjunction with ``tonumpy()`` to update any modifications to the picture array"""
1465        self._data.GetPointData().GetScalars().Modified()
1466        return self

Use in conjunction with tonumpy() to update any modifications to the picture array

def write(self, filename):
1468    def write(self, filename):
1469        """Write picture to file as png or jpg."""
1470        vedo.file_io.write(self._data, filename)
1471        self.pipeline = utils.OperationNode(
1472            "write", comment=filename[:15], parents=[self], c="#8a817c", shape="cylinder"
1473        )
1474        return self

Write picture to file as png or jpg.

class Picture2D(vedo.base.BaseActor2D):
144class Picture2D(vedo.BaseActor2D):
145    """
146    Embed an image as a static 2D image in the canvas.
147    """
148
149    def __init__(self, fig, pos=(0, 0), scale=1, ontop=False, padding=1, justify=""):
150        """
151        Embed an image as a static 2D image in the canvas.
152
153        Arguments:
154            fig : Picture, matplotlib.Figure, matplotlib.pyplot, vtkImageData
155                the input image
156            pos : (list)
157                2D (x,y) position in range [0,1],
158                [0,0] being the bottom-left corner
159            scale : (float)
160                apply a scaling factor to the image
161            ontop : (bool)
162                keep image on top or not
163            padding : (int)
164                an internal padding space as a fraction of size
165                (matplotlib only)
166            justify : (str)
167                define the anchor point ("top-left", "top-center", ...)
168        """
169        vedo.BaseActor2D.__init__(self)
170        # print("input type:", fig.__class__)
171
172        self.array = None
173
174        if utils.is_sequence(fig):
175            self.array = fig
176            self._data = _get_img(self.array)
177
178        elif isinstance(fig, Picture):
179            self._data = fig.inputdata()
180
181        elif isinstance(fig, vtk.vtkImageData):
182            assert fig.GetDimensions()[2] == 1, "Cannot create an Picture2D from Volume"
183            self._data = fig
184
185        elif isinstance(fig, str):
186            self._data = _get_img(fig)
187            self.filename = fig
188
189        elif "matplotlib" in str(fig.__class__):
190            if hasattr(fig, "gcf"):
191                fig = fig.gcf()
192            fig.tight_layout(pad=padding)
193            fig.canvas.draw()
194
195            # self.array = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8)
196            # self.array = self.array.reshape(fig.canvas.get_width_height()[::-1] + (3,))
197            width, height = fig.get_size_inches() * fig.get_dpi()
198            self.array = np.frombuffer(fig.canvas.buffer_rgba(), dtype=np.uint8).reshape(
199                (int(height), int(width), 4)
200            )
201            self.array = self.array[:, :, :3]
202
203            self._data = _get_img(self.array)
204
205        #############
206        if scale != 1:
207            newsize = np.array(self._data.GetDimensions()[:2]) * scale
208            newsize = newsize.astype(int)
209            rsz = vtk.vtkImageResize()
210            rsz.SetInputData(self._data)
211            rsz.SetResizeMethodToOutputDimensions()
212            rsz.SetOutputDimensions(newsize[0], newsize[1], 1)
213            rsz.Update()
214            self._data = rsz.GetOutput()
215
216        if padding:
217            pass  # TODO
218
219        if justify:
220            self._data, pos = _set_justification(self._data, justify)
221        else:
222            self._data, pos = _set_justification(self._data, pos)
223
224        self._mapper = vtk.vtkImageMapper()
225        # self._mapper.RenderToRectangleOn() # NOT good because of aliasing
226        self._mapper.SetInputData(self._data)
227        self._mapper.SetColorWindow(255)
228        self._mapper.SetColorLevel(127.5)
229        self.SetMapper(self._mapper)
230
231        self.GetPositionCoordinate().SetCoordinateSystem(3)
232        self.SetPosition(pos)
233
234        if ontop:
235            self.GetProperty().SetDisplayLocationToForeground()
236        else:
237            self.GetProperty().SetDisplayLocationToBackground()
238
239    @property
240    def shape(self):
241        return np.array(self._data.GetDimensions()[:2]).astype(int)

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

Picture2D(fig, pos=(0, 0), scale=1, ontop=False, padding=1, justify='')
149    def __init__(self, fig, pos=(0, 0), scale=1, ontop=False, padding=1, justify=""):
150        """
151        Embed an image as a static 2D image in the canvas.
152
153        Arguments:
154            fig : Picture, matplotlib.Figure, matplotlib.pyplot, vtkImageData
155                the input image
156            pos : (list)
157                2D (x,y) position in range [0,1],
158                [0,0] being the bottom-left corner
159            scale : (float)
160                apply a scaling factor to the image
161            ontop : (bool)
162                keep image on top or not
163            padding : (int)
164                an internal padding space as a fraction of size
165                (matplotlib only)
166            justify : (str)
167                define the anchor point ("top-left", "top-center", ...)
168        """
169        vedo.BaseActor2D.__init__(self)
170        # print("input type:", fig.__class__)
171
172        self.array = None
173
174        if utils.is_sequence(fig):
175            self.array = fig
176            self._data = _get_img(self.array)
177
178        elif isinstance(fig, Picture):
179            self._data = fig.inputdata()
180
181        elif isinstance(fig, vtk.vtkImageData):
182            assert fig.GetDimensions()[2] == 1, "Cannot create an Picture2D from Volume"
183            self._data = fig
184
185        elif isinstance(fig, str):
186            self._data = _get_img(fig)
187            self.filename = fig
188
189        elif "matplotlib" in str(fig.__class__):
190            if hasattr(fig, "gcf"):
191                fig = fig.gcf()
192            fig.tight_layout(pad=padding)
193            fig.canvas.draw()
194
195            # self.array = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8)
196            # self.array = self.array.reshape(fig.canvas.get_width_height()[::-1] + (3,))
197            width, height = fig.get_size_inches() * fig.get_dpi()
198            self.array = np.frombuffer(fig.canvas.buffer_rgba(), dtype=np.uint8).reshape(
199                (int(height), int(width), 4)
200            )
201            self.array = self.array[:, :, :3]
202
203            self._data = _get_img(self.array)
204
205        #############
206        if scale != 1:
207            newsize = np.array(self._data.GetDimensions()[:2]) * scale
208            newsize = newsize.astype(int)
209            rsz = vtk.vtkImageResize()
210            rsz.SetInputData(self._data)
211            rsz.SetResizeMethodToOutputDimensions()
212            rsz.SetOutputDimensions(newsize[0], newsize[1], 1)
213            rsz.Update()
214            self._data = rsz.GetOutput()
215
216        if padding:
217            pass  # TODO
218
219        if justify:
220            self._data, pos = _set_justification(self._data, justify)
221        else:
222            self._data, pos = _set_justification(self._data, pos)
223
224        self._mapper = vtk.vtkImageMapper()
225        # self._mapper.RenderToRectangleOn() # NOT good because of aliasing
226        self._mapper.SetInputData(self._data)
227        self._mapper.SetColorWindow(255)
228        self._mapper.SetColorLevel(127.5)
229        self.SetMapper(self._mapper)
230
231        self.GetPositionCoordinate().SetCoordinateSystem(3)
232        self.SetPosition(pos)
233
234        if ontop:
235            self.GetProperty().SetDisplayLocationToForeground()
236        else:
237            self.GetProperty().SetDisplayLocationToBackground()

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

Arguments:
  • fig : Picture, matplotlib.Figure, matplotlib.pyplot, vtkImageData the input image
  • pos : (list) 2D (x,y) position in range [0,1], [0,0] being the bottom-left corner
  • scale : (float) apply a scaling factor to the image
  • ontop : (bool) keep image on top or not
  • padding : (int) an internal padding space as a fraction of size (matplotlib only)
  • justify : (str) define the anchor point ("top-left", "top-center", ...)