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

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

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

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):
387    def inputdata(self):
388        """Return the underlying ``vtkImagaData`` object."""
389        return self._data

Return the underlying vtkImagaData object.

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

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

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

Return the number of channels in picture

def clone(self, transformed=False):
409    def clone(self, transformed=False):
410        """Return an exact copy of the input Picture.
411        If transform is True, it is given the same scaling and position."""
412        img = vtk.vtkImageData()
413        img.DeepCopy(self._data)
414        pic = Picture(img)
415        if transformed:
416            # assign the same transformation to the copy
417            pic.SetOrigin(self.GetOrigin())
418            pic.SetScale(self.GetScale())
419            pic.SetOrientation(self.GetOrientation())
420            pic.SetPosition(self.GetPosition())
421        
422        pic.pipeline = utils.OperationNode(
423            "clone", parents=[self], c="#f7dada", shape="diamond")
424        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):
426    def cmap(self, name, vmin=None, vmax=None):
427        """Colorize a picture with a colormap representing pixel intensity"""
428        n = self._data.GetPointData().GetNumberOfComponents()
429        if n > 1:
430            ecr = vtk.vtkImageExtractComponents()
431            ecr.SetInputData(self._data)
432            ecr.SetComponents(0, 1, 2)
433            ecr.Update()
434            ilum = vtk.vtkImageMagnitude()
435            ilum.SetInputData(self._data)
436            ilum.Update()
437            img = ilum.GetOutput()
438        else:
439            img = self._data
440
441        lut = vtk.vtkLookupTable()
442        _vmin, _vmax = img.GetScalarRange()
443        if vmin is not None:
444            _vmin = vmin
445        if vmax is not None:
446            _vmax = vmax
447        lut.SetRange(_vmin, _vmax)
448
449        ncols = 256
450        lut.SetNumberOfTableValues(ncols)
451        cols = colors.color_map(range(ncols), name, 0, ncols)
452        for i, c in enumerate(cols):
453            lut.SetTableValue(i, *c)
454        lut.Build()
455
456        imap = vtk.vtkImageMapToColors()
457        imap.SetLookupTable(lut)
458        imap.SetInputData(img)
459        imap.Update()
460        self._update(imap.GetOutput())
461        self.pipeline = utils.OperationNode(
462            f"cmap", comment=f'"{name}"', parents=[self], c="#f28482")
463        return self

Colorize a picture with a colormap representing pixel intensity

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

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

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

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

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

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

def window(self, value=None):
491    def window(self, value=None):
492        """Get/Set the image color window (contrast) in the rendering scene."""
493        if value is None:
494            return self.GetProperty().GetColorWindow()
495        self.GetProperty().SetColorWindow(value)
496        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):
498    def crop(self, top=None, bottom=None, right=None, left=None, pixels=False):
499        """Crop picture.
500
501        Arguments:
502            top : (float)
503                fraction to crop from the top margin
504            bottom : (float)
505                fraction to crop from the bottom margin
506            left : (float)
507                fraction to crop from the left margin
508            right : (float)
509                fraction to crop from the right margin
510            pixels : (bool)
511                units are pixels
512        """
513        extractVOI = vtk.vtkExtractVOI()
514        extractVOI.SetInputData(self._data)
515        extractVOI.IncludeBoundaryOn()
516
517        d = self.GetInput().GetDimensions()
518        if pixels:
519            extractVOI.SetVOI(left, d[0]-right-1, bottom, d[1]-top-1, 0, 0)
520        else:
521            bx0, bx1, by0, by1 = 0, d[0]-1, 0, d[1]-1
522            if left is not None:   bx0 = int((d[0]-1)*left)
523            if right is not None:  bx1 = int((d[0]-1)*(1-right))
524            if bottom is not None: by0 = int((d[1]-1)*bottom)
525            if top is not None:    by1 = int((d[1]-1)*(1-top))
526            extractVOI.SetVOI(bx0, bx1, by0, by1, 0, 0)
527        extractVOI.Update()
528
529        self.shape = extractVOI.GetOutput().GetDimensions()[:2]
530        self._update(extractVOI.GetOutput())
531        self.pipeline = utils.OperationNode(
532            "crop", comment=f"shape={tuple(self.shape)}", parents=[self], c="#f28482")
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 , optional
542                number of pixels to be added (or a list of length 4)
543            value : (int), optional
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], y0 - pixels[2], y1 + pixels[3], 0, 0
553            )
554        else:
555            pf.SetOutputWholeExtent(x0 - pixels, x1 + pixels, y0 - pixels, y1 + pixels, 0, 0)
556        pf.Update()
557        self._update(pf.GetOutput())
558        self.pipeline = utils.OperationNode(
559            "pad", comment=f"{pixels} pixels", parents=[self], c="#f28482")
560        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 , optional number of pixels to be added (or a list of length 4)
  • value : (int), optional intensity value (gray-scale color) of the padding
def tile(self, nx=4, ny=4, shift=(0, 0)):
562    def tile(self, nx=4, ny=4, shift=(0, 0)):
563        """
564        Generate a tiling from the current picture by mirroring and repeating it.
565
566        Arguments:
567            nx : (float)
568                number of repeats along x
569            ny : (float)
570                number of repeats along x
571            shift : (list)
572                shift in x and y in pixels
573        """
574        x0, x1, y0, y1, z0, z1 = self._data.GetExtent()
575        constant_pad = vtk.vtkImageMirrorPad()
576        constant_pad.SetInputData(self._data)
577        constant_pad.SetOutputWholeExtent(
578            int(x0 + shift[0] + 0.5),
579            int(x1 * nx + shift[0] + 0.5),
580            int(y0 + shift[1] + 0.5),
581            int(y1 * ny + shift[1] + 0.5),
582            z0,
583            z1,
584        )
585        constant_pad.Update()
586        pic = Picture(constant_pad.GetOutput())
587
588        pic.pipeline = utils.OperationNode(
589            "tile", comment=f"by {nx}x{ny}", parents=[self], c="#f28482")
590        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):
592    def append(self, pictures, axis="z", preserve_extents=False):
593        """
594        Append the input images to the current one along the specified axis.
595        Except for the append axis, all inputs must have the same extent.
596        All inputs must have the same number of scalar components.
597        The output has the same origin and spacing as the first input.
598        The origin and spacing of all other inputs are ignored.
599        All inputs must have the same scalar type.
600
601        Arguments:
602            axis : (int, str)
603                axis expanded to hold the multiple images
604            preserve_extents : (bool)
605                if True, the extent of the inputs is used to place
606                the image in the output. The whole extent of the output is the union of the input
607                whole extents. Any portion of the output not covered by the inputs is set to zero.
608                The origin and spacing is taken from the first input.
609
610        Example:
611            ```python
612            from vedo import Picture, dataurl
613            pic = Picture(dataurl+'dog.jpg').pad()
614            pic.append([pic,pic], axis='y')
615            pic.append([pic,pic,pic], axis='x')
616            pic.show(axes=1).close()
617            ```
618            ![](https://vedo.embl.es/images/feats/pict_append.png)
619        """
620        ima = vtk.vtkImageAppend()
621        ima.SetInputData(self._data)
622        if not utils.is_sequence(pictures):
623            pictures = [pictures]
624        for p in pictures:
625            if isinstance(p, vtk.vtkImageData):
626                ima.AddInputData(p)
627            else:
628                ima.AddInputData(p.inputdata())
629        ima.SetPreserveExtents(preserve_extents)
630        if axis == "x":
631            axis = 0
632        elif axis == "y":
633            axis = 1
634        ima.SetAppendAxis(axis)
635        ima.Update()
636        self._update(ima.GetOutput())
637        self.pipeline = utils.OperationNode(
638            "append", comment=f"axis={axis}", parents=[self, *pictures], c="#f28482")
639        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):
641    def resize(self, newsize):
642        """Resize the image resolution by specifying the number of pixels in width and height.
643        If left to zero, it will be automatically calculated to keep the original aspect ratio.
644
645        newsize is the shape of picture as [npx, npy], or it can be also expressed as a fraction.
646        """
647        old_dims = np.array(self._data.GetDimensions())
648
649        if not utils.is_sequence(newsize):
650            newsize = (old_dims * newsize + 0.5).astype(int)
651
652        if not newsize[1]:
653            ar = old_dims[1] / old_dims[0]
654            newsize = [newsize[0], int(newsize[0] * ar + 0.5)]
655        if not newsize[0]:
656            ar = old_dims[0] / old_dims[1]
657            newsize = [int(newsize[1] * ar + 0.5), newsize[1]]
658        newsize = [newsize[0], newsize[1], old_dims[2]]
659
660        rsz = vtk.vtkImageResize()
661        rsz.SetInputData(self._data)
662        rsz.SetResizeMethodToOutputDimensions()
663        rsz.SetOutputDimensions(newsize)
664        rsz.Update()
665        out = rsz.GetOutput()
666        out.SetSpacing(1, 1, 1)
667        self._update(out)
668        self.pipeline = utils.OperationNode(
669            "resize", comment=f"shape={tuple(self.shape)}", parents=[self], c="#f28482")
670        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'):
672    def mirror(self, axis="x"):
673        """Mirror picture along x or y axis. Same as `flip()`."""
674        ff = vtk.vtkImageFlip()
675        ff.SetInputData(self.inputdata())
676        if axis.lower() == "x":
677            ff.SetFilteredAxis(0)
678        elif axis.lower() == "y":
679            ff.SetFilteredAxis(1)
680        else:
681            colors.printc("Error in mirror(): mirror must be set to x or y.", c="r")
682            raise RuntimeError()
683        ff.Update()
684        self._update(ff.GetOutput())
685        self.pipeline = utils.OperationNode(f"mirror {axis}", parents=[self], c="#f28482")
686        return self

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

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

Select one single component of the rgb image.

def bw(self):
748    def bw(self):
749        """Make it black and white using luminance calibration."""
750        n = self._data.GetPointData().GetNumberOfComponents()
751        if n == 4:
752            ecr = vtk.vtkImageExtractComponents()
753            ecr.SetInputData(self._data)
754            ecr.SetComponents(0, 1, 2)
755            ecr.Update()
756            img = ecr.GetOutput()
757        else:
758            img = self._data
759
760        ecr = vtk.vtkImageLuminance()
761        ecr.SetInputData(img)
762        ecr.Update()
763        self._update(ecr.GetOutput())
764        self.pipeline = utils.OperationNode("black&white", parents=[self], c="#f28482")
765        return self

Make it black and white using luminance calibration.

def smooth(self, sigma=3, radius=None):
767    def smooth(self, sigma=3, radius=None):
768        """
769        Smooth a Picture with Gaussian kernel.
770
771        Arguments:
772            sigma : (int)
773                number of sigmas in pixel units
774            radius : (float)
775                how far out the gaussian kernel will go before being clamped to zero
776        """
777        gsf = vtk.vtkImageGaussianSmooth()
778        gsf.SetDimensionality(2)
779        gsf.SetInputData(self._data)
780        if radius is not None:
781            if utils.is_sequence(radius):
782                gsf.SetRadiusFactors(radius[0], radius[1])
783            else:
784                gsf.SetRadiusFactor(radius)
785
786        if utils.is_sequence(sigma):
787            gsf.SetStandardDeviations(sigma[0], sigma[1])
788        else:
789            gsf.SetStandardDeviation(sigma)
790        gsf.Update()
791        self._update(gsf.GetOutput())
792        self.pipeline = utils.OperationNode(
793            "smooth", comment=f"sigma={sigma}", parents=[self], c="#f28482")
794        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):
796    def median(self):
797        """
798        Median filter that preserves thin lines and corners.
799
800        It operates on a 5x5 pixel neighborhood. It computes two values initially:
801        the median of the + neighbors and the median of the x neighbors.
802        It then computes the median of these two values plus the center pixel.
803        This result of this second median is the output pixel value.
804        """
805        medf = vtk.vtkImageHybridMedian2D()
806        medf.SetInputData(self._data)
807        medf.Update()
808        self._update(medf.GetOutput())
809        self.pipeline = utils.OperationNode("median", parents=[self], c="#f28482")
810        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):
812    def enhance(self):
813        """
814        Enhance a b&w picture using the laplacian, enhancing high-freq edges.
815
816        Example:
817            ```python
818            from vedo import *
819            pic = Picture(vedo.dataurl+'images/dog.jpg').bw()
820            show(pic, pic.clone().enhance(), N=2, mode='image', zoom='tight')
821            ```
822            ![](https://vedo.embl.es/images/feats/pict_enhance.png)
823        """
824        img = self._data
825        scalarRange = img.GetPointData().GetScalars().GetRange()
826
827        cast = vtk.vtkImageCast()
828        cast.SetInputData(img)
829        cast.SetOutputScalarTypeToDouble()
830        cast.Update()
831
832        laplacian = vtk.vtkImageLaplacian()
833        laplacian.SetInputData(cast.GetOutput())
834        laplacian.SetDimensionality(2)
835        laplacian.Update()
836
837        subtr = vtk.vtkImageMathematics()
838        subtr.SetInputData(0, cast.GetOutput())
839        subtr.SetInputData(1, laplacian.GetOutput())
840        subtr.SetOperationToSubtract()
841        subtr.Update()
842
843        color_window = scalarRange[1] - scalarRange[0]
844        color_level = color_window / 2
845        original_color = vtk.vtkImageMapToWindowLevelColors()
846        original_color.SetWindow(color_window)
847        original_color.SetLevel(color_level)
848        original_color.SetInputData(subtr.GetOutput())
849        original_color.Update()
850        self._update(original_color.GetOutput())
851
852        self.pipeline = utils.OperationNode(
853            "enhance", parents=[self], c="#f28482")
854        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):
856    def fft(self, mode="magnitude", logscale=12, center=True):
857        """
858        Fast Fourier transform of a picture.
859
860        Arguments:
861            logscale : (float)
862                if non-zero, take the logarithm of the intensity and scale it by this factor.
863            mode : (str)
864                either [magnitude, real, imaginary, complex], compute the point array data accordingly.
865            center : (bool)
866                shift constant zero-frequency to the center of the image for display.
867                (FFT converts spatial images into frequency space, but puts the zero frequency at the origin)
868        """
869        ffti = vtk.vtkImageFFT()
870        ffti.SetInputData(self._data)
871        ffti.Update()
872
873        if "mag" in mode:
874            mag = vtk.vtkImageMagnitude()
875            mag.SetInputData(ffti.GetOutput())
876            mag.Update()
877            out = mag.GetOutput()
878        elif "real" in mode:
879            erf = vtk.vtkImageExtractComponents()
880            erf.SetInputData(ffti.GetOutput())
881            erf.SetComponents(0)
882            erf.Update()
883            out = erf.GetOutput()
884        elif "imaginary" in mode:
885            eimf = vtk.vtkImageExtractComponents()
886            eimf.SetInputData(ffti.GetOutput())
887            eimf.SetComponents(1)
888            eimf.Update()
889            out = eimf.GetOutput()
890        elif "complex" in mode:
891            out = ffti.GetOutput()
892        else:
893            colors.printc("Error in fft(): unknown mode", mode)
894            raise RuntimeError()
895
896        if center:
897            center = vtk.vtkImageFourierCenter()
898            center.SetInputData(out)
899            center.Update()
900            out = center.GetOutput()
901
902        if "complex" not in mode:
903            if logscale:
904                ils = vtk.vtkImageLogarithmicScale()
905                ils.SetInputData(out)
906                ils.SetConstant(logscale)
907                ils.Update()
908                out = ils.GetOutput()
909
910        pic = Picture(out)
911        pic.pipeline = utils.OperationNode("FFT", parents=[self], c="#f28482")
912        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'):
914    def rfft(self, mode="magnitude"):
915        """Reverse Fast Fourier transform of a picture."""
916
917        ffti = vtk.vtkImageRFFT()
918        ffti.SetInputData(self._data)
919        ffti.Update()
920
921        if "mag" in mode:
922            mag = vtk.vtkImageMagnitude()
923            mag.SetInputData(ffti.GetOutput())
924            mag.Update()
925            out = mag.GetOutput()
926        elif "real" in mode:
927            erf = vtk.vtkImageExtractComponents()
928            erf.SetInputData(ffti.GetOutput())
929            erf.SetComponents(0)
930            erf.Update()
931            out = erf.GetOutput()
932        elif "imaginary" in mode:
933            eimf = vtk.vtkImageExtractComponents()
934            eimf.SetInputData(ffti.GetOutput())
935            eimf.SetComponents(1)
936            eimf.Update()
937            out = eimf.GetOutput()
938        elif "complex" in mode:
939            out = ffti.GetOutput()
940        else:
941            colors.printc("Error in rfft(): unknown mode", mode)
942            raise RuntimeError()
943
944        pic = Picture(out)
945        pic.pipeline = utils.OperationNode("rFFT", parents=[self], c="#f28482")
946        return pic

Reverse Fast Fourier transform of a picture.

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

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

def binarize(self, threshold=None, invert=False):
1113    def binarize(self, threshold=None, invert=False):
1114        """
1115        Return a new Picture where pixel above threshold are set to 255
1116        and pixels below are set to 0.
1117
1118        Arguments:
1119            threshold : (float)
1120                input threshold value
1121            invert : (bool)
1122                invert threshold direction
1123
1124        Example:
1125            ```python
1126            from vedo import Picture, show
1127            pic1 = Picture("https://aws.glamour.es/prod/designs/v1/assets/620x459/547577.jpg")
1128            pic2 = pic1.clone().invert()
1129            pic3 = pic1.clone().binarize()
1130            show(pic1, pic2, pic3, N=3, bg="blue9").close()
1131            ```
1132            ![](https://vedo.embl.es/images/feats/pict_binarize.png)
1133        """
1134        rgb = self.tonumpy()
1135        if rgb.ndim == 3:
1136            intensity = np.sum(rgb, axis=2) / 3
1137        else:
1138            intensity = rgb
1139
1140        if threshold is None:
1141            vmin, vmax = np.min(intensity), np.max(intensity)
1142            threshold = (vmax + vmin) / 2
1143
1144        data = np.zeros_like(intensity).astype(np.uint8)
1145        mask = np.where(intensity > threshold)
1146        if invert:
1147            data += 255
1148            data[mask] = 0
1149        else:
1150            data[mask] = 255
1151
1152        self._update(_get_img(data, flip=True))
1153        
1154        self.pipeline = utils.OperationNode(
1155            "binarize", comment=f"threshold={threshold}", parents=[self], c="#f28482")
1156        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):
1158    def threshold(self, value=None, flip=False):
1159        """
1160        Create a polygonal Mesh from a Picture by filling regions with pixels
1161        luminosity above a specified value.
1162
1163        Arguments:
1164            value : (float)
1165                The default is None, e.i. 1/3 of the scalar range.
1166            flip: (bool)
1167                Flip polygon orientations
1168
1169        Returns:
1170            A polygonal mesh.
1171        """
1172        mgf = vtk.vtkImageMagnitude()
1173        mgf.SetInputData(self._data)
1174        mgf.Update()
1175        msq = vtk.vtkMarchingSquares()
1176        msq.SetInputData(mgf.GetOutput())
1177        if value is None:
1178            r0, r1 = self._data.GetScalarRange()
1179            value = r0 + (r1 - r0) / 3
1180        msq.SetValue(0, value)
1181        msq.Update()
1182        if flip:
1183            rs = vtk.vtkReverseSense()
1184            rs.SetInputData(msq.GetOutput())
1185            rs.ReverseCellsOn()
1186            rs.ReverseNormalsOff()
1187            rs.Update()
1188            output = rs.GetOutput()
1189        else:
1190            output = msq.GetOutput()
1191        ctr = vtk.vtkContourTriangulator()
1192        ctr.SetInputData(output)
1193        ctr.Update()
1194        out = vedo.Mesh(ctr.GetOutput(), c="k").bc("t").lighting("off")
1195
1196        out.pipeline = utils.OperationNode(
1197            "threshold", comment=f"{value: .2f}", parents=[self], c="#f28482:#e9c46a")
1198        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):
1200    def tomesh(self):
1201        """
1202        Convert an image to polygonal data (quads),
1203        with each polygon vertex assigned a RGBA value.
1204        """
1205        dims = self._data.GetDimensions()
1206        gr = vedo.shapes.Grid(s=dims[:2], res=(dims[0] - 1, dims[1] - 1))
1207        gr.pos(int(dims[0] / 2), int(dims[1] / 2)).pickable(True).wireframe(False).lw(0)
1208        self._data.GetPointData().GetScalars().SetName("RGBA")
1209        gr.inputdata().GetPointData().AddArray(self._data.GetPointData().GetScalars())
1210        gr.inputdata().GetPointData().SetActiveScalars("RGBA")
1211        gr.mapper().SetArrayName("RGBA")
1212        gr.mapper().SetScalarModeToUsePointData()
1213        gr.mapper().ScalarVisibilityOn()
1214        gr.name = self.name
1215        gr.filename = self.filename
1216        gr.pipeline = utils.OperationNode("tomesh", parents=[self], c="#f28482:#e9c46a")
1217        return gr

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

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

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

def triangle(self, p1, p2, p3, c='red3', alpha=1):
1328    def triangle(self, p1, p2, p3, c="red3", alpha=1):
1329        """Draw a triangle on top of current image. Units are pixels."""
1330        x1, y1 = p1
1331        x2, y2 = p2
1332        x3, y3 = p3
1333
1334        r, g, b = vedo.colors.get_color(c)
1335        c = np.array([r, g, b]) * 255
1336        c = c.astype(np.uint8)
1337
1338        alpha = min(alpha, 1)
1339        if alpha <= 0:
1340            return self
1341        alpha2 = alpha
1342        alpha1 = 1 - alpha
1343
1344        nx, ny = self.dimensions()
1345        x1 = min(x1, nx)
1346        x2 = min(x2, nx)
1347        x3 = min(x3, nx)
1348
1349        y1 = min(y1, ny)
1350        y2 = min(y2, ny)
1351        y3 = min(y3, ny)
1352
1353        nchan = self.channels()
1354        narrayA = self.tonumpy()
1355
1356        canvas_source = vtk.vtkImageCanvasSource2D()
1357        canvas_source.SetExtent(0, nx - 1, 0, ny - 1, 0, 0)
1358        canvas_source.SetScalarTypeToUnsignedChar()
1359        canvas_source.SetNumberOfScalarComponents(nchan)
1360        canvas_source.SetDrawColor(255, 255, 255)
1361        canvas_source.FillTriangle(x1, y1, x2, y2, x3, y3)
1362        canvas_source.Update()
1363        image_data = canvas_source.GetOutput()
1364
1365        vscals = image_data.GetPointData().GetScalars()
1366        narrayB = vedo.utils.vtk2numpy(vscals).reshape(ny, nx, nchan)
1367        narrayB = np.flip(narrayB, axis=0)
1368        narrayC = np.where(narrayB < 255, narrayA, alpha1 * narrayA + alpha2 * c)
1369        self._update(_get_img(narrayC))
1370        self.pipeline = utils.OperationNode("triangle", parents=[self], c="#f28482")
1371        return self

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

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

Add text to an image.

def modified(self):
1445    def modified(self):
1446        """Use in conjunction with ``tonumpy()`` to update any modifications to the picture array"""
1447        self._data.GetPointData().GetScalars().Modified()
1448        return self

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

def write(self, filename):
1450    def write(self, filename):
1451        """Write picture to file as png or jpg."""
1452        vedo.io.write(self._data, filename)
1453        self.pipeline = utils.OperationNode(
1454            "write", comment=filename[:15], parents=[self], 
1455            c="#8a817c", shape='cylinder')
1456        return self

Write picture to file as png or jpg.