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