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