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 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}:   </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 / window </b></td><td>" 380 + str(self.level()) + " / " + 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  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  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  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  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
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}:   </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 / window </b></td><td>" 381 + str(self.level()) + " / " + 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  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  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  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  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.
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)
387 def inputdata(self): 388 """Return the underlying ``vtkImagaData`` object.""" 389 return self._data
Return the underlying vtkImagaData
object.
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
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
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.
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
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]
.
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.
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.
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.
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
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
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
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  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()
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.
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()
.
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()
.
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
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.
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.
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
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.
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  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')
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)
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.
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
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.
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
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).
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  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()
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.
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.
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.
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  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()
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.
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.
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.
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
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.