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