vkit.element.image
1# Copyright 2022 vkit-x Administrator. All Rights Reserved. 2# 3# This project (vkit-x/vkit) is dual-licensed under commercial and SSPL licenses. 4# 5# The commercial license gives you the full rights to create and distribute software 6# on your own terms without any SSPL license obligations. For more information, 7# please see the "LICENSE_COMMERCIAL.txt" file. 8# 9# This project is also available under Server Side Public License (SSPL). 10# The SSPL licensing is ideal for use cases such as open source projects with 11# SSPL distribution, student/academic purposes, hobby projects, internal research 12# projects without external distribution, or other projects where all SSPL 13# obligations can be met. For more information, please see the "LICENSE_SSPL.txt" file. 14from typing import cast, Mapping, Sequence, Tuple, Union, Optional, Iterable, List, TypeVar 15from enum import Enum, unique 16from collections import abc 17from contextlib import ContextDecorator 18 19import attrs 20import numpy as np 21from PIL import ( 22 Image as PilImage, 23 ImageOps as PilImageOps, 24) 25import cv2 as cv 26import iolite as io 27 28from vkit.utility import PathType 29from .type import Shapable, ElementSetOperationMode 30from .opt import generate_shape_and_resized_shape 31 32 33@unique 34class ImageMode(Enum): 35 RGB = 'rgb' 36 RGB_GCN = 'rgb_gcn' 37 RGBA = 'rgba' 38 HSV = 'hsv' 39 HSV_GCN = 'hsv_gcn' 40 HSL = 'hsl' 41 HSL_GCN = 'hsl_gcn' 42 GRAYSCALE = 'grayscale' 43 GRAYSCALE_GCN = 'grayscale_gcn' 44 NONE = 'none' 45 46 def to_ndim(self): 47 if self in _IMAGE_MODE_NDIM_3: 48 return 3 49 elif self in _IMAGE_MODE_NDIM_2: 50 return 2 51 else: 52 raise NotImplementedError() 53 54 def to_dtype(self): 55 if self in _IMAGE_MODE_DTYPE_UINT8: 56 return np.uint8 57 elif self in _IMAGE_MODE_DTYPE_FLOAT32: 58 return np.float32 59 else: 60 raise NotImplementedError() 61 62 def to_num_channels(self): 63 if self in _IMAGE_MODE_NUM_CHANNELS_4: 64 return 4 65 elif self in _IMAGE_MODE_NUM_CHANNELS_3: 66 return 3 67 elif self in _IMAGE_MODE_NUM_CHANNELS_2: 68 return None 69 else: 70 raise NotImplementedError 71 72 def supports_gcn_mode(self): 73 return self not in _IMAGE_MODE_NON_GCN_TO_GCN 74 75 def to_gcn_mode(self): 76 if not self.supports_gcn_mode(): 77 raise RuntimeError(f'image_mode={self} not supported.') 78 return _IMAGE_MODE_NON_GCN_TO_GCN[self] 79 80 def in_gcn_mode(self): 81 return self in _IMAGE_MODE_GCN_TO_NON_GCN 82 83 def to_non_gcn_mode(self): 84 if not self.in_gcn_mode(): 85 raise RuntimeError(f'image_mode={self} not in gcn mode.') 86 return _IMAGE_MODE_GCN_TO_NON_GCN[self] 87 88 89_IMAGE_MODE_NDIM_3 = { 90 ImageMode.RGB, 91 ImageMode.RGB_GCN, 92 ImageMode.RGBA, 93 ImageMode.HSV, 94 ImageMode.HSV_GCN, 95 ImageMode.HSL, 96 ImageMode.HSL_GCN, 97} 98 99_IMAGE_MODE_NDIM_2 = { 100 ImageMode.GRAYSCALE, 101 ImageMode.GRAYSCALE_GCN, 102} 103 104_IMAGE_MODE_DTYPE_UINT8 = { 105 ImageMode.RGB, 106 ImageMode.RGBA, 107 ImageMode.HSV, 108 ImageMode.HSL, 109 ImageMode.GRAYSCALE, 110} 111 112_IMAGE_MODE_DTYPE_FLOAT32 = { 113 ImageMode.RGB_GCN, 114 ImageMode.HSV_GCN, 115 ImageMode.HSL_GCN, 116 ImageMode.GRAYSCALE_GCN, 117} 118 119_IMAGE_MODE_NUM_CHANNELS_4 = { 120 ImageMode.RGBA, 121} 122 123_IMAGE_MODE_NUM_CHANNELS_3 = { 124 ImageMode.RGB, 125 ImageMode.RGB_GCN, 126 ImageMode.HSV, 127 ImageMode.HSV_GCN, 128 ImageMode.HSL, 129 ImageMode.HSL_GCN, 130} 131 132_IMAGE_MODE_NUM_CHANNELS_2 = { 133 ImageMode.GRAYSCALE, 134 ImageMode.GRAYSCALE_GCN, 135} 136 137_IMAGE_MODE_NON_GCN_TO_GCN = { 138 ImageMode.RGB: ImageMode.RGB_GCN, 139 ImageMode.HSV: ImageMode.HSV_GCN, 140 ImageMode.HSL: ImageMode.HSL_GCN, 141 ImageMode.GRAYSCALE: ImageMode.GRAYSCALE_GCN, 142} 143 144_IMAGE_MODE_GCN_TO_NON_GCN = {val: key for key, val in _IMAGE_MODE_NON_GCN_TO_GCN.items()} 145 146 147@attrs.define 148class ImageSetItemConfig: 149 value: Union[ 150 'Image', 151 np.ndarray, 152 Tuple[int, ...], 153 int, 154 ] # yapf: disable 155 alpha: Union[np.ndarray, float] = 1.0 156 157 158class WritableImageContextDecorator(ContextDecorator): 159 160 def __init__(self, image: 'Image'): 161 super().__init__() 162 self.image = image 163 164 def __enter__(self): 165 if self.image.mat.flags.c_contiguous: 166 assert not self.image.mat.flags.writeable 167 168 try: 169 self.image.mat.flags.writeable = True 170 except ValueError: 171 # Copy on write. 172 object.__setattr__( 173 self.image, 174 'mat', 175 np.array(self.image.mat), 176 ) 177 assert self.image.mat.flags.writeable 178 179 def __exit__(self, *exc): # type: ignore 180 self.image.mat.flags.writeable = False 181 182 183_IMAGE_SRC_MODE_TO_PRE_SLICE: Mapping[ImageMode, Sequence[int]] = { 184 # HSL -> HLS. 185 ImageMode.HSL: [0, 2, 1], 186} 187 188_IMAGE_SRC_MODE_TO_RGB_CV_CODE = { 189 ImageMode.GRAYSCALE: cv.COLOR_GRAY2RGB, 190 ImageMode.RGBA: cv.COLOR_RGBA2RGB, 191 ImageMode.HSV: cv.COLOR_HSV2RGB_FULL, 192 # NOTE: HSL need pre-slicing. 193 ImageMode.HSL: cv.COLOR_HLS2RGB_FULL, 194} 195 196_IMAGE_INV_DST_MODE_TO_RGB_CV_CODE = { 197 ImageMode.GRAYSCALE: cv.COLOR_RGB2GRAY, 198 ImageMode.RGBA: cv.COLOR_RGB2RGBA, 199 ImageMode.HSV: cv.COLOR_RGB2HSV_FULL, 200 # NOTE: HSL need post-slicing. 201 ImageMode.HSL: cv.COLOR_RGB2HLS_FULL, 202} 203 204_IMAGE_DST_MODE_TO_POST_SLICE: Mapping[ImageMode, Sequence[int]] = { 205 # HLS -> HSL. 206 ImageMode.HSL: [0, 2, 1], 207} 208 209_IMAGE_SRC_DST_MODE_TO_CV_CODE: Mapping[Tuple[ImageMode, ImageMode], int] = { 210 (ImageMode.GRAYSCALE, ImageMode.RGBA): cv.COLOR_GRAY2RGBA, 211 (ImageMode.RGBA, ImageMode.GRAYSCALE): cv.COLOR_RGBA2GRAY, 212} 213 214_E = TypeVar('_E', 'Box', 'Polygon', 'Mask', 'ScoreMap') 215 216 217@attrs.define(frozen=True, eq=False) 218class Image(Shapable): 219 mat: np.ndarray 220 mode: ImageMode = ImageMode.NONE 221 box: Optional['Box'] = None 222 223 def __attrs_post_init__(self): 224 if self.mode != ImageMode.NONE: 225 # Validate mat.dtype and mode. 226 assert self.mode.to_dtype() == self.mat.dtype 227 assert self.mode.to_ndim() == self.mat.ndim 228 229 else: 230 # Infer image mode based on mat. 231 if self.mat.dtype == np.float32: 232 raise NotImplementedError('mode is None and mat.dtype == np.float32.') 233 234 elif self.mat.dtype == np.uint8: 235 if self.mat.ndim == 2: 236 # Defaults to GRAYSCALE. 237 mode = ImageMode.GRAYSCALE 238 elif self.mat.ndim == 3: 239 if self.mat.shape[2] == 4: 240 mode = ImageMode.RGBA 241 elif self.mat.shape[2] == 3: 242 # Defaults to RGB. 243 mode = ImageMode.RGB 244 else: 245 raise NotImplementedError(f'Invalid num_channels={self.mat.shape[2]}.') 246 else: 247 raise NotImplementedError(f'mat.ndim={self.mat.ndim} not supported.') 248 249 object.__setattr__(self, 'mode', mode) 250 251 else: 252 raise NotImplementedError(f'Invalid mat.dtype={self.mat.dtype}.') 253 254 # For the control of write. 255 self.mat.flags.writeable = False 256 257 if self.box and self.shape != self.box.shape: 258 raise RuntimeError('self.shape != box.shape.') 259 260 ############### 261 # Constructor # 262 ############### 263 @classmethod 264 def from_shape( 265 cls, 266 shape: Tuple[int, int], 267 num_channels: int = 3, 268 value: Union[Tuple[int, ...], int] = 255, 269 ): 270 height, width = shape 271 272 if num_channels == 0: 273 mat_shape = (height, width) 274 275 else: 276 assert num_channels > 0 277 278 if isinstance(value, tuple): 279 assert len(value) == num_channels 280 281 mat_shape = (height, width, num_channels) 282 283 mat = np.full(mat_shape, fill_value=value, dtype=np.uint8) 284 return cls(mat=mat) 285 286 @classmethod 287 def from_shapable( 288 cls, 289 shapable: Shapable, 290 num_channels: int = 3, 291 value: Union[Tuple[int, ...], int] = 255, 292 ): 293 return cls.from_shape( 294 shape=shapable.shape, 295 num_channels=num_channels, 296 value=value, 297 ) 298 299 ############ 300 # Property # 301 ############ 302 @property 303 def height(self): 304 return self.mat.shape[0] 305 306 @property 307 def width(self): 308 return self.mat.shape[1] 309 310 @property 311 def num_channels(self): 312 if self.mat.ndim == 2: 313 return 0 314 else: 315 assert self.mat.ndim == 3 316 return self.mat.shape[2] 317 318 @property 319 def writable_context(self): 320 return WritableImageContextDecorator(self) 321 322 ############## 323 # Conversion # 324 ############## 325 @classmethod 326 def from_pil_image(cls, pil_image: PilImage.Image): 327 # NOTE: Make a copy explicitly, otherwise is not writable. 328 mat = np.array(pil_image, dtype=np.uint8) 329 return cls(mat=mat) 330 331 def to_pil_image(self): 332 return PilImage.fromarray(self.mat) 333 334 @classmethod 335 def from_file(cls, path: PathType, disable_exif_orientation: bool = False): 336 # NOTE: PilImage.open cannot handle `~`. 337 path = io.file(path).expanduser() 338 339 pil_image = PilImage.open(str(path)) 340 pil_image.load() 341 342 if not disable_exif_orientation: 343 # https://exiftool.org/TagNames/EXIF.html 344 # https://github.com/python-pillow/Pillow/blob/main/src/PIL/ImageOps.py#L571 345 # Avoid unnecessary copy. 346 if pil_image.getexif().get(0x0112): 347 pil_image = PilImageOps.exif_transpose(pil_image) 348 349 return cls.from_pil_image(pil_image) 350 351 def to_file(self, path: PathType, disable_to_rgb_image: bool = False): 352 image = self 353 if not disable_to_rgb_image: 354 image = image.to_rgb_image() 355 356 pil_image = image.to_pil_image() 357 358 path = io.file(path).expanduser() 359 pil_image.save(str(path)) 360 361 ############ 362 # Operator # 363 ############ 364 def copy(self): 365 return attrs.evolve(self, mat=self.mat.copy()) 366 367 def assign_mat(self, mat: np.ndarray): 368 with self.writable_context: 369 object.__setattr__(self, 'mat', mat) 370 371 @classmethod 372 def check_values_and_alphas_uniqueness( 373 cls, 374 values: Sequence[Union['Image', np.ndarray, Tuple[int, ...], int, float]], 375 alphas: Sequence[Union['ScoreMap', np.ndarray, float]], 376 ): 377 return check_elements_uniqueness(values) and check_elements_uniqueness(alphas) 378 379 @classmethod 380 def unpack_element_value_tuples( 381 cls, 382 element_value_tuples: Iterable[ 383 Union[ 384 Tuple[ 385 _E, 386 Union['Image', np.ndarray, Tuple[int, ...], int], 387 ], 388 Tuple[ 389 _E, 390 Union['Image', np.ndarray, Tuple[int, ...], int], 391 Union[float, np.ndarray], 392 ], 393 ] 394 ], 395 ): # yapf: disable 396 elements: List[_E] = [] 397 values: List[Union[Image, np.ndarray, Tuple[int, ...], int]] = [] 398 alphas: List[Union[float, np.ndarray]] = [] 399 400 for element_value_tuple in element_value_tuples: 401 if len(element_value_tuple) == 2: 402 element, value = element_value_tuple 403 alpha = 1.0 404 else: 405 element, value, alpha = element_value_tuple 406 elements.append(element) 407 values.append(value) 408 alphas.append(alpha) 409 410 return elements, values, alphas 411 412 def fill_by_box_value_tuples( 413 self, 414 box_value_tuples: Iterable[ 415 Union[ 416 Tuple[ 417 'Box', 418 Union['Image', np.ndarray, Tuple[int, ...], int], 419 ], 420 Tuple[ 421 'Box', 422 Union['Image', np.ndarray, Tuple[int, ...], int], 423 Union[float, np.ndarray], 424 ], 425 ] 426 ], 427 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 428 skip_values_uniqueness_check: bool = False, 429 ): # yapf: disable 430 boxes, values, alphas = self.unpack_element_value_tuples(box_value_tuples) 431 432 boxes_mask = generate_fill_by_boxes_mask(self.shape, boxes, mode) 433 if boxes_mask is None: 434 for box, value, alpha in zip(boxes, values, alphas): 435 box.fill_image( 436 image=self, 437 value=value, 438 alpha=alpha, 439 ) 440 441 else: 442 unique = True 443 if not skip_values_uniqueness_check: 444 unique = self.check_values_and_alphas_uniqueness(values, alphas) 445 446 if unique: 447 boxes_mask.fill_image( 448 image=self, 449 value=values[0], 450 alpha=alphas[0], 451 ) 452 else: 453 for box, value, alpha in zip(boxes, values, alphas): 454 box_mask = box.extract_mask(boxes_mask).to_box_attached(box) 455 box_mask.fill_image( 456 image=self, 457 value=value, 458 alpha=alpha, 459 ) 460 461 def fill_by_boxes( 462 self, 463 boxes: Iterable['Box'], 464 value: Union['Image', np.ndarray, Tuple[int, ...], int], 465 alpha: Union[np.ndarray, float] = 1.0, 466 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 467 ): 468 self.fill_by_box_value_tuples( 469 box_value_tuples=((box, value, alpha) for box in boxes), 470 mode=mode, 471 skip_values_uniqueness_check=True, 472 ) 473 474 def fill_by_polygon_value_tuples( 475 self, 476 polygon_value_tuples: Iterable[ 477 Union[ 478 Tuple[ 479 'Polygon', 480 Union['Image', np.ndarray, Tuple[int, ...], int], 481 ], 482 Tuple[ 483 'Polygon', 484 Union['Image', np.ndarray, Tuple[int, ...], int], 485 Union[float, np.ndarray], 486 ], 487 ] 488 ], 489 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 490 skip_values_uniqueness_check: bool = False, 491 ): # yapf: disable 492 polygons, values, alphas = self.unpack_element_value_tuples(polygon_value_tuples) 493 494 polygons_mask = generate_fill_by_polygons_mask(self.shape, polygons, mode) 495 if polygons_mask is None: 496 for polygon, value, alpha in zip(polygons, values, alphas): 497 polygon.fill_image( 498 image=self, 499 value=value, 500 alpha=alpha, 501 ) 502 503 else: 504 unique = True 505 if not skip_values_uniqueness_check: 506 unique = self.check_values_and_alphas_uniqueness(values, alphas) 507 508 if unique: 509 polygons_mask.fill_image( 510 image=self, 511 value=values[0], 512 alpha=alphas[0], 513 ) 514 else: 515 for polygon, value, alpha in zip(polygons, values, alphas): 516 bounding_box = polygon.to_bounding_box() 517 polygon_mask = bounding_box.extract_mask(polygons_mask) 518 polygon_mask = polygon_mask.to_box_attached(bounding_box) 519 polygon_mask.fill_image( 520 image=self, 521 value=value, 522 alpha=alpha, 523 ) 524 525 def fill_by_polygons( 526 self, 527 polygons: Iterable['Polygon'], 528 value: Union['Image', np.ndarray, Tuple[int, ...], int], 529 alpha: Union[np.ndarray, float] = 1.0, 530 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 531 ): 532 self.fill_by_polygon_value_tuples( 533 polygon_value_tuples=((polygon, value, alpha) for polygon in polygons), 534 mode=mode, 535 skip_values_uniqueness_check=True, 536 ) 537 538 def fill_by_mask_value_tuples( 539 self, 540 mask_value_tuples: Iterable[ 541 Union[ 542 Tuple[ 543 'Mask', 544 Union['Image', np.ndarray, Tuple[int, ...], int], 545 ], 546 Tuple[ 547 'Mask', 548 Union['Image', np.ndarray, Tuple[int, ...], int], 549 Union[float, np.ndarray], 550 ], 551 ] 552 ], 553 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 554 skip_values_uniqueness_check: bool = False, 555 ): # yapf: disable 556 masks, values, alphas = self.unpack_element_value_tuples(mask_value_tuples) 557 558 masks_mask = generate_fill_by_masks_mask(self.shape, masks, mode) 559 if masks_mask is None: 560 for mask, value, alpha in zip(masks, values, alphas): 561 mask.fill_image( 562 image=self, 563 value=value, 564 alpha=alpha, 565 ) 566 567 else: 568 unique = True 569 if not skip_values_uniqueness_check: 570 unique = self.check_values_and_alphas_uniqueness(values, alphas) 571 572 if unique: 573 masks_mask.fill_image( 574 image=self, 575 value=values[0], 576 alpha=alphas[0], 577 ) 578 else: 579 for mask, value, alpha in zip(masks, values, alphas): 580 if mask.box: 581 boxed_mask = mask.box.extract_mask(masks_mask) 582 else: 583 boxed_mask = masks_mask 584 585 boxed_mask = boxed_mask.copy() 586 mask.to_inverted_mask().fill_mask(boxed_mask, value=0) 587 boxed_mask.fill_image( 588 image=self, 589 value=value, 590 alpha=alpha, 591 ) 592 593 def fill_by_masks( 594 self, 595 masks: Iterable['Mask'], 596 value: Union['Image', np.ndarray, Tuple[int, ...], int], 597 alpha: Union[np.ndarray, float] = 1.0, 598 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 599 ): 600 self.fill_by_mask_value_tuples( 601 mask_value_tuples=((mask, value, alpha) for mask in masks), 602 mode=mode, 603 skip_values_uniqueness_check=True, 604 ) 605 606 def fill_by_score_map_value_tuples( 607 self, 608 score_map_value_tuples: Iterable[ 609 Tuple[ 610 'ScoreMap', 611 Union['Image', np.ndarray, Tuple[int, ...], int], 612 ] 613 ], 614 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 615 skip_values_uniqueness_check: bool = False, 616 ): # yapf: disable 617 # NOTE: score maps serve as masks & alphas, hence ignoring unpacked alphas. 618 score_maps, values, _ = self.unpack_element_value_tuples(score_map_value_tuples) 619 620 score_maps_mask = generate_fill_by_score_maps_mask(self.shape, score_maps, mode) 621 if score_maps_mask is None: 622 for score_map, value in zip(score_maps, values): 623 score_map.fill_image( 624 image=self, 625 value=value, 626 ) 627 628 else: 629 unique = True 630 if not skip_values_uniqueness_check: 631 unique = check_elements_uniqueness(values) 632 633 if unique: 634 # This is unlikely to happen. 635 score_maps_mask.fill_image( 636 image=self, 637 value=values[0], 638 alpha=score_maps[0], 639 ) 640 else: 641 for score_map, value in zip(score_maps, values): 642 if score_map.box: 643 boxed_mask = score_map.box.extract_mask(score_maps_mask) 644 else: 645 boxed_mask = score_maps_mask 646 647 boxed_mask = boxed_mask.copy() 648 score_map.to_mask().to_inverted_mask().fill_mask(boxed_mask, value=0) 649 boxed_mask.fill_image( 650 image=self, 651 value=value, 652 alpha=score_map, 653 ) 654 655 def fill_by_score_maps( 656 self, 657 score_maps: Iterable['ScoreMap'], 658 value: Union['Image', np.ndarray, Tuple[int, ...], int], 659 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 660 ): 661 self.fill_by_score_map_value_tuples( 662 score_map_value_tuples=((score_map, value) for score_map in score_maps), 663 mode=mode, 664 skip_values_uniqueness_check=True, 665 ) 666 667 def __setitem__( 668 self, 669 element: Union[ 670 'Box', 671 'Polygon', 672 'Mask', 673 'ScoreMap', 674 ], 675 config: Union[ 676 'Image', 677 np.ndarray, 678 Tuple[int, ...], 679 int, 680 ImageSetItemConfig, 681 ], 682 ): # yapf: disable 683 if not isinstance(config, ImageSetItemConfig): 684 value = config 685 alpha = 1.0 686 else: 687 assert isinstance(config, ImageSetItemConfig) 688 value = config.value 689 alpha = config.alpha 690 691 if isinstance(value, tuple): 692 assert value 693 assert isinstance(value[0], int) 694 else: 695 assert not isinstance(value, abc.Iterable) 696 697 # Type inference cannot handle this case. 698 value = cast( 699 Union[ 700 'Image', 701 np.ndarray, 702 Tuple[int, ...], 703 int, 704 ], 705 value 706 ) # yapf: disable 707 assert not isinstance(alpha, abc.Iterable) 708 709 if isinstance(element, ScoreMap): 710 element.fill_image(image=self, value=value) 711 else: 712 element.fill_image(image=self, value=value, alpha=alpha) 713 714 def __getitem__( 715 self, 716 element: Union[ 717 'Box', 718 'Polygon', 719 'Mask', 720 ], 721 ): # yapf: disable 722 return element.extract_image(self) 723 724 def to_box_attached(self, box: 'Box'): 725 assert self.height == box.height 726 assert self.width == box.width 727 return attrs.evolve(self, box=box) 728 729 def to_box_detached(self): 730 assert self.box 731 return attrs.evolve(self, box=None) 732 733 def to_gcn_image( 734 self, 735 lamb: float = 0, 736 eps: float = 1E-8, 737 scale: float = 1.0, 738 ): 739 # Global contrast normalization. 740 # https://cedar.buffalo.edu/~srihari/CSE676/12.2%20Computer%20Vision.pdf 741 # (H, W) or (H, W, 3) 742 mode = self.mode.to_gcn_mode() 743 744 mat = self.mat.astype(np.float32) 745 746 # Normalize mean(contrast). 747 mean = np.mean(mat) 748 mat -= mean 749 750 # Std normalized. 751 std = np.sqrt(lamb + np.mean(mat**2)) 752 mat /= max(eps, std) 753 if scale != 1.0: 754 mat *= scale 755 756 return Image(mat=mat, mode=mode) 757 758 def to_non_gcn_image(self): 759 mode = self.mode.to_non_gcn_mode() 760 761 assert self.mat.dtype == np.float32 762 val_min = np.min(self.mat) 763 mat = self.mat - val_min 764 gap = np.max(mat) 765 mat = mat / gap * 255.0 766 mat = np.round(mat) 767 mat = np.clip(mat, 0, 255).astype(np.uint8) 768 769 return Image(mat=mat, mode=mode) # type: ignore 770 771 def to_target_mode_image(self, target_mode: ImageMode): 772 if target_mode == self.mode: 773 # Identity. 774 return self 775 776 skip_copy = False 777 if self.mode.in_gcn_mode(): 778 self = self.to_non_gcn_image() 779 skip_copy = True 780 781 if self.mode == target_mode: 782 # GCN to non-GCN conversion. 783 return self if skip_copy else self.copy() 784 785 mat = self.mat 786 787 # Pre-slicing. 788 if self.mode in _IMAGE_SRC_MODE_TO_PRE_SLICE: 789 mat: np.ndarray = mat[:, :, _IMAGE_SRC_MODE_TO_PRE_SLICE[self.mode]] 790 791 if (self.mode, target_mode) in _IMAGE_SRC_DST_MODE_TO_CV_CODE: 792 # Shortcut. 793 cv_code = _IMAGE_SRC_DST_MODE_TO_CV_CODE[(self.mode, target_mode)] 794 dst_mat: np.ndarray = cv.cvtColor(mat, cv_code) 795 return Image(mat=dst_mat, mode=target_mode) 796 797 dst_mat = mat 798 if self.mode != ImageMode.RGB: 799 # Convert to RGB. 800 dst_mat: np.ndarray = cv.cvtColor(mat, _IMAGE_SRC_MODE_TO_RGB_CV_CODE[self.mode]) 801 802 if target_mode == ImageMode.RGB: 803 # No need to continue. 804 return Image(mat=dst_mat, mode=ImageMode.RGB) 805 806 # Convert RGB to target mode. 807 assert target_mode in _IMAGE_INV_DST_MODE_TO_RGB_CV_CODE 808 dst_mat: np.ndarray = cv.cvtColor(dst_mat, _IMAGE_INV_DST_MODE_TO_RGB_CV_CODE[target_mode]) 809 810 # Post-slicing. 811 if target_mode in _IMAGE_DST_MODE_TO_POST_SLICE: 812 dst_mat: np.ndarray = dst_mat[:, :, _IMAGE_DST_MODE_TO_POST_SLICE[target_mode]] 813 814 return Image(mat=dst_mat, mode=target_mode) 815 816 def to_grayscale_image(self): 817 return self.to_target_mode_image(ImageMode.GRAYSCALE) 818 819 def to_rgb_image(self): 820 return self.to_target_mode_image(ImageMode.RGB) 821 822 def to_rgba_image(self): 823 return self.to_target_mode_image(ImageMode.RGBA) 824 825 def to_hsv_image(self): 826 return self.to_target_mode_image(ImageMode.HSV) 827 828 def to_hsl_image(self): 829 return self.to_target_mode_image(ImageMode.HSL) 830 831 def to_shifted_image(self, offset_y: int = 0, offset_x: int = 0): 832 assert self.box 833 shifted_box = self.box.to_shifted_box(offset_y=offset_y, offset_x=offset_x) 834 return attrs.evolve(self, box=shifted_box) 835 836 def to_resized_image( 837 self, 838 resized_height: Optional[int] = None, 839 resized_width: Optional[int] = None, 840 cv_resize_interpolation: int = cv.INTER_CUBIC, 841 ): 842 _, _, resized_height, resized_width = generate_shape_and_resized_shape( 843 shapable_or_shape=self, 844 resized_height=resized_height, 845 resized_width=resized_width, 846 ) 847 mat = cv.resize( 848 self.mat, 849 (resized_width, resized_height), 850 interpolation=cv_resize_interpolation, 851 ) 852 return attrs.evolve(self, mat=mat) 853 854 def to_conducted_resized_image( 855 self, 856 shapable_or_shape: Union[Shapable, Tuple[int, int]], 857 resized_height: Optional[int] = None, 858 resized_width: Optional[int] = None, 859 cv_resize_interpolation: int = cv.INTER_CUBIC, 860 ): 861 assert self.box 862 resized_box = self.box.to_conducted_resized_box( 863 shapable_or_shape=shapable_or_shape, 864 resized_height=resized_height, 865 resized_width=resized_width, 866 ) 867 resized_image = self.to_box_detached().to_resized_image( 868 resized_height=resized_box.height, 869 resized_width=resized_box.width, 870 cv_resize_interpolation=cv_resize_interpolation, 871 ) 872 resized_image = resized_image.to_box_attached(resized_box) 873 return resized_image 874 875 def to_cropped_image( 876 self, 877 up: Optional[int] = None, 878 down: Optional[int] = None, 879 left: Optional[int] = None, 880 right: Optional[int] = None, 881 ): 882 assert not self.box 883 884 up = up or 0 885 down = down or self.height - 1 886 left = left or 0 887 right = right or self.width - 1 888 889 return attrs.evolve(self, mat=self.mat[up:down + 1, left:right + 1]) 890 891 892# Cyclic dependency, by design. 893from .uniqueness import check_elements_uniqueness # noqa: E402 894from .box import Box, generate_fill_by_boxes_mask # noqa: E402 895from .polygon import Polygon, generate_fill_by_polygons_mask # noqa: E402 896from .mask import Mask, generate_fill_by_masks_mask # noqa: E402 897from .score_map import ScoreMap, generate_fill_by_score_maps_mask # noqa: E402
class
ImageMode(enum.Enum):
35class ImageMode(Enum): 36 RGB = 'rgb' 37 RGB_GCN = 'rgb_gcn' 38 RGBA = 'rgba' 39 HSV = 'hsv' 40 HSV_GCN = 'hsv_gcn' 41 HSL = 'hsl' 42 HSL_GCN = 'hsl_gcn' 43 GRAYSCALE = 'grayscale' 44 GRAYSCALE_GCN = 'grayscale_gcn' 45 NONE = 'none' 46 47 def to_ndim(self): 48 if self in _IMAGE_MODE_NDIM_3: 49 return 3 50 elif self in _IMAGE_MODE_NDIM_2: 51 return 2 52 else: 53 raise NotImplementedError() 54 55 def to_dtype(self): 56 if self in _IMAGE_MODE_DTYPE_UINT8: 57 return np.uint8 58 elif self in _IMAGE_MODE_DTYPE_FLOAT32: 59 return np.float32 60 else: 61 raise NotImplementedError() 62 63 def to_num_channels(self): 64 if self in _IMAGE_MODE_NUM_CHANNELS_4: 65 return 4 66 elif self in _IMAGE_MODE_NUM_CHANNELS_3: 67 return 3 68 elif self in _IMAGE_MODE_NUM_CHANNELS_2: 69 return None 70 else: 71 raise NotImplementedError 72 73 def supports_gcn_mode(self): 74 return self not in _IMAGE_MODE_NON_GCN_TO_GCN 75 76 def to_gcn_mode(self): 77 if not self.supports_gcn_mode(): 78 raise RuntimeError(f'image_mode={self} not supported.') 79 return _IMAGE_MODE_NON_GCN_TO_GCN[self] 80 81 def in_gcn_mode(self): 82 return self in _IMAGE_MODE_GCN_TO_NON_GCN 83 84 def to_non_gcn_mode(self): 85 if not self.in_gcn_mode(): 86 raise RuntimeError(f'image_mode={self} not in gcn mode.') 87 return _IMAGE_MODE_GCN_TO_NON_GCN[self]
An enumeration.
RGB =
<ImageMode.RGB: 'rgb'>
RGB_GCN =
<ImageMode.RGB_GCN: 'rgb_gcn'>
RGBA =
<ImageMode.RGBA: 'rgba'>
HSV =
<ImageMode.HSV: 'hsv'>
HSV_GCN =
<ImageMode.HSV_GCN: 'hsv_gcn'>
HSL =
<ImageMode.HSL: 'hsl'>
HSL_GCN =
<ImageMode.HSL_GCN: 'hsl_gcn'>
GRAYSCALE =
<ImageMode.GRAYSCALE: 'grayscale'>
GRAYSCALE_GCN =
<ImageMode.GRAYSCALE_GCN: 'grayscale_gcn'>
NONE =
<ImageMode.NONE: 'none'>
Inherited Members
- enum.Enum
- name
- value
class
ImageSetItemConfig:
149class ImageSetItemConfig: 150 value: Union[ 151 'Image', 152 np.ndarray, 153 Tuple[int, ...], 154 int, 155 ] # yapf: disable 156 alpha: Union[np.ndarray, float] = 1.0
ImageSetItemConfig( value: Union[vkit.element.image.Image, numpy.ndarray, Tuple[int, ...], int], alpha: Union[numpy.ndarray, float] = 1.0)
2def __init__(self, value, alpha=attr_dict['alpha'].default): 3 self.value = value 4 self.alpha = alpha
Method generated by attrs for class ImageSetItemConfig.
class
WritableImageContextDecorator(contextlib.ContextDecorator):
159class WritableImageContextDecorator(ContextDecorator): 160 161 def __init__(self, image: 'Image'): 162 super().__init__() 163 self.image = image 164 165 def __enter__(self): 166 if self.image.mat.flags.c_contiguous: 167 assert not self.image.mat.flags.writeable 168 169 try: 170 self.image.mat.flags.writeable = True 171 except ValueError: 172 # Copy on write. 173 object.__setattr__( 174 self.image, 175 'mat', 176 np.array(self.image.mat), 177 ) 178 assert self.image.mat.flags.writeable 179 180 def __exit__(self, *exc): # type: ignore 181 self.image.mat.flags.writeable = False
A base class or mixin that enables context managers to work as decorators.
WritableImageContextDecorator(image: vkit.element.image.Image)
219class Image(Shapable): 220 mat: np.ndarray 221 mode: ImageMode = ImageMode.NONE 222 box: Optional['Box'] = None 223 224 def __attrs_post_init__(self): 225 if self.mode != ImageMode.NONE: 226 # Validate mat.dtype and mode. 227 assert self.mode.to_dtype() == self.mat.dtype 228 assert self.mode.to_ndim() == self.mat.ndim 229 230 else: 231 # Infer image mode based on mat. 232 if self.mat.dtype == np.float32: 233 raise NotImplementedError('mode is None and mat.dtype == np.float32.') 234 235 elif self.mat.dtype == np.uint8: 236 if self.mat.ndim == 2: 237 # Defaults to GRAYSCALE. 238 mode = ImageMode.GRAYSCALE 239 elif self.mat.ndim == 3: 240 if self.mat.shape[2] == 4: 241 mode = ImageMode.RGBA 242 elif self.mat.shape[2] == 3: 243 # Defaults to RGB. 244 mode = ImageMode.RGB 245 else: 246 raise NotImplementedError(f'Invalid num_channels={self.mat.shape[2]}.') 247 else: 248 raise NotImplementedError(f'mat.ndim={self.mat.ndim} not supported.') 249 250 object.__setattr__(self, 'mode', mode) 251 252 else: 253 raise NotImplementedError(f'Invalid mat.dtype={self.mat.dtype}.') 254 255 # For the control of write. 256 self.mat.flags.writeable = False 257 258 if self.box and self.shape != self.box.shape: 259 raise RuntimeError('self.shape != box.shape.') 260 261 ############### 262 # Constructor # 263 ############### 264 @classmethod 265 def from_shape( 266 cls, 267 shape: Tuple[int, int], 268 num_channels: int = 3, 269 value: Union[Tuple[int, ...], int] = 255, 270 ): 271 height, width = shape 272 273 if num_channels == 0: 274 mat_shape = (height, width) 275 276 else: 277 assert num_channels > 0 278 279 if isinstance(value, tuple): 280 assert len(value) == num_channels 281 282 mat_shape = (height, width, num_channels) 283 284 mat = np.full(mat_shape, fill_value=value, dtype=np.uint8) 285 return cls(mat=mat) 286 287 @classmethod 288 def from_shapable( 289 cls, 290 shapable: Shapable, 291 num_channels: int = 3, 292 value: Union[Tuple[int, ...], int] = 255, 293 ): 294 return cls.from_shape( 295 shape=shapable.shape, 296 num_channels=num_channels, 297 value=value, 298 ) 299 300 ############ 301 # Property # 302 ############ 303 @property 304 def height(self): 305 return self.mat.shape[0] 306 307 @property 308 def width(self): 309 return self.mat.shape[1] 310 311 @property 312 def num_channels(self): 313 if self.mat.ndim == 2: 314 return 0 315 else: 316 assert self.mat.ndim == 3 317 return self.mat.shape[2] 318 319 @property 320 def writable_context(self): 321 return WritableImageContextDecorator(self) 322 323 ############## 324 # Conversion # 325 ############## 326 @classmethod 327 def from_pil_image(cls, pil_image: PilImage.Image): 328 # NOTE: Make a copy explicitly, otherwise is not writable. 329 mat = np.array(pil_image, dtype=np.uint8) 330 return cls(mat=mat) 331 332 def to_pil_image(self): 333 return PilImage.fromarray(self.mat) 334 335 @classmethod 336 def from_file(cls, path: PathType, disable_exif_orientation: bool = False): 337 # NOTE: PilImage.open cannot handle `~`. 338 path = io.file(path).expanduser() 339 340 pil_image = PilImage.open(str(path)) 341 pil_image.load() 342 343 if not disable_exif_orientation: 344 # https://exiftool.org/TagNames/EXIF.html 345 # https://github.com/python-pillow/Pillow/blob/main/src/PIL/ImageOps.py#L571 346 # Avoid unnecessary copy. 347 if pil_image.getexif().get(0x0112): 348 pil_image = PilImageOps.exif_transpose(pil_image) 349 350 return cls.from_pil_image(pil_image) 351 352 def to_file(self, path: PathType, disable_to_rgb_image: bool = False): 353 image = self 354 if not disable_to_rgb_image: 355 image = image.to_rgb_image() 356 357 pil_image = image.to_pil_image() 358 359 path = io.file(path).expanduser() 360 pil_image.save(str(path)) 361 362 ############ 363 # Operator # 364 ############ 365 def copy(self): 366 return attrs.evolve(self, mat=self.mat.copy()) 367 368 def assign_mat(self, mat: np.ndarray): 369 with self.writable_context: 370 object.__setattr__(self, 'mat', mat) 371 372 @classmethod 373 def check_values_and_alphas_uniqueness( 374 cls, 375 values: Sequence[Union['Image', np.ndarray, Tuple[int, ...], int, float]], 376 alphas: Sequence[Union['ScoreMap', np.ndarray, float]], 377 ): 378 return check_elements_uniqueness(values) and check_elements_uniqueness(alphas) 379 380 @classmethod 381 def unpack_element_value_tuples( 382 cls, 383 element_value_tuples: Iterable[ 384 Union[ 385 Tuple[ 386 _E, 387 Union['Image', np.ndarray, Tuple[int, ...], int], 388 ], 389 Tuple[ 390 _E, 391 Union['Image', np.ndarray, Tuple[int, ...], int], 392 Union[float, np.ndarray], 393 ], 394 ] 395 ], 396 ): # yapf: disable 397 elements: List[_E] = [] 398 values: List[Union[Image, np.ndarray, Tuple[int, ...], int]] = [] 399 alphas: List[Union[float, np.ndarray]] = [] 400 401 for element_value_tuple in element_value_tuples: 402 if len(element_value_tuple) == 2: 403 element, value = element_value_tuple 404 alpha = 1.0 405 else: 406 element, value, alpha = element_value_tuple 407 elements.append(element) 408 values.append(value) 409 alphas.append(alpha) 410 411 return elements, values, alphas 412 413 def fill_by_box_value_tuples( 414 self, 415 box_value_tuples: Iterable[ 416 Union[ 417 Tuple[ 418 'Box', 419 Union['Image', np.ndarray, Tuple[int, ...], int], 420 ], 421 Tuple[ 422 'Box', 423 Union['Image', np.ndarray, Tuple[int, ...], int], 424 Union[float, np.ndarray], 425 ], 426 ] 427 ], 428 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 429 skip_values_uniqueness_check: bool = False, 430 ): # yapf: disable 431 boxes, values, alphas = self.unpack_element_value_tuples(box_value_tuples) 432 433 boxes_mask = generate_fill_by_boxes_mask(self.shape, boxes, mode) 434 if boxes_mask is None: 435 for box, value, alpha in zip(boxes, values, alphas): 436 box.fill_image( 437 image=self, 438 value=value, 439 alpha=alpha, 440 ) 441 442 else: 443 unique = True 444 if not skip_values_uniqueness_check: 445 unique = self.check_values_and_alphas_uniqueness(values, alphas) 446 447 if unique: 448 boxes_mask.fill_image( 449 image=self, 450 value=values[0], 451 alpha=alphas[0], 452 ) 453 else: 454 for box, value, alpha in zip(boxes, values, alphas): 455 box_mask = box.extract_mask(boxes_mask).to_box_attached(box) 456 box_mask.fill_image( 457 image=self, 458 value=value, 459 alpha=alpha, 460 ) 461 462 def fill_by_boxes( 463 self, 464 boxes: Iterable['Box'], 465 value: Union['Image', np.ndarray, Tuple[int, ...], int], 466 alpha: Union[np.ndarray, float] = 1.0, 467 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 468 ): 469 self.fill_by_box_value_tuples( 470 box_value_tuples=((box, value, alpha) for box in boxes), 471 mode=mode, 472 skip_values_uniqueness_check=True, 473 ) 474 475 def fill_by_polygon_value_tuples( 476 self, 477 polygon_value_tuples: Iterable[ 478 Union[ 479 Tuple[ 480 'Polygon', 481 Union['Image', np.ndarray, Tuple[int, ...], int], 482 ], 483 Tuple[ 484 'Polygon', 485 Union['Image', np.ndarray, Tuple[int, ...], int], 486 Union[float, np.ndarray], 487 ], 488 ] 489 ], 490 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 491 skip_values_uniqueness_check: bool = False, 492 ): # yapf: disable 493 polygons, values, alphas = self.unpack_element_value_tuples(polygon_value_tuples) 494 495 polygons_mask = generate_fill_by_polygons_mask(self.shape, polygons, mode) 496 if polygons_mask is None: 497 for polygon, value, alpha in zip(polygons, values, alphas): 498 polygon.fill_image( 499 image=self, 500 value=value, 501 alpha=alpha, 502 ) 503 504 else: 505 unique = True 506 if not skip_values_uniqueness_check: 507 unique = self.check_values_and_alphas_uniqueness(values, alphas) 508 509 if unique: 510 polygons_mask.fill_image( 511 image=self, 512 value=values[0], 513 alpha=alphas[0], 514 ) 515 else: 516 for polygon, value, alpha in zip(polygons, values, alphas): 517 bounding_box = polygon.to_bounding_box() 518 polygon_mask = bounding_box.extract_mask(polygons_mask) 519 polygon_mask = polygon_mask.to_box_attached(bounding_box) 520 polygon_mask.fill_image( 521 image=self, 522 value=value, 523 alpha=alpha, 524 ) 525 526 def fill_by_polygons( 527 self, 528 polygons: Iterable['Polygon'], 529 value: Union['Image', np.ndarray, Tuple[int, ...], int], 530 alpha: Union[np.ndarray, float] = 1.0, 531 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 532 ): 533 self.fill_by_polygon_value_tuples( 534 polygon_value_tuples=((polygon, value, alpha) for polygon in polygons), 535 mode=mode, 536 skip_values_uniqueness_check=True, 537 ) 538 539 def fill_by_mask_value_tuples( 540 self, 541 mask_value_tuples: Iterable[ 542 Union[ 543 Tuple[ 544 'Mask', 545 Union['Image', np.ndarray, Tuple[int, ...], int], 546 ], 547 Tuple[ 548 'Mask', 549 Union['Image', np.ndarray, Tuple[int, ...], int], 550 Union[float, np.ndarray], 551 ], 552 ] 553 ], 554 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 555 skip_values_uniqueness_check: bool = False, 556 ): # yapf: disable 557 masks, values, alphas = self.unpack_element_value_tuples(mask_value_tuples) 558 559 masks_mask = generate_fill_by_masks_mask(self.shape, masks, mode) 560 if masks_mask is None: 561 for mask, value, alpha in zip(masks, values, alphas): 562 mask.fill_image( 563 image=self, 564 value=value, 565 alpha=alpha, 566 ) 567 568 else: 569 unique = True 570 if not skip_values_uniqueness_check: 571 unique = self.check_values_and_alphas_uniqueness(values, alphas) 572 573 if unique: 574 masks_mask.fill_image( 575 image=self, 576 value=values[0], 577 alpha=alphas[0], 578 ) 579 else: 580 for mask, value, alpha in zip(masks, values, alphas): 581 if mask.box: 582 boxed_mask = mask.box.extract_mask(masks_mask) 583 else: 584 boxed_mask = masks_mask 585 586 boxed_mask = boxed_mask.copy() 587 mask.to_inverted_mask().fill_mask(boxed_mask, value=0) 588 boxed_mask.fill_image( 589 image=self, 590 value=value, 591 alpha=alpha, 592 ) 593 594 def fill_by_masks( 595 self, 596 masks: Iterable['Mask'], 597 value: Union['Image', np.ndarray, Tuple[int, ...], int], 598 alpha: Union[np.ndarray, float] = 1.0, 599 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 600 ): 601 self.fill_by_mask_value_tuples( 602 mask_value_tuples=((mask, value, alpha) for mask in masks), 603 mode=mode, 604 skip_values_uniqueness_check=True, 605 ) 606 607 def fill_by_score_map_value_tuples( 608 self, 609 score_map_value_tuples: Iterable[ 610 Tuple[ 611 'ScoreMap', 612 Union['Image', np.ndarray, Tuple[int, ...], int], 613 ] 614 ], 615 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 616 skip_values_uniqueness_check: bool = False, 617 ): # yapf: disable 618 # NOTE: score maps serve as masks & alphas, hence ignoring unpacked alphas. 619 score_maps, values, _ = self.unpack_element_value_tuples(score_map_value_tuples) 620 621 score_maps_mask = generate_fill_by_score_maps_mask(self.shape, score_maps, mode) 622 if score_maps_mask is None: 623 for score_map, value in zip(score_maps, values): 624 score_map.fill_image( 625 image=self, 626 value=value, 627 ) 628 629 else: 630 unique = True 631 if not skip_values_uniqueness_check: 632 unique = check_elements_uniqueness(values) 633 634 if unique: 635 # This is unlikely to happen. 636 score_maps_mask.fill_image( 637 image=self, 638 value=values[0], 639 alpha=score_maps[0], 640 ) 641 else: 642 for score_map, value in zip(score_maps, values): 643 if score_map.box: 644 boxed_mask = score_map.box.extract_mask(score_maps_mask) 645 else: 646 boxed_mask = score_maps_mask 647 648 boxed_mask = boxed_mask.copy() 649 score_map.to_mask().to_inverted_mask().fill_mask(boxed_mask, value=0) 650 boxed_mask.fill_image( 651 image=self, 652 value=value, 653 alpha=score_map, 654 ) 655 656 def fill_by_score_maps( 657 self, 658 score_maps: Iterable['ScoreMap'], 659 value: Union['Image', np.ndarray, Tuple[int, ...], int], 660 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 661 ): 662 self.fill_by_score_map_value_tuples( 663 score_map_value_tuples=((score_map, value) for score_map in score_maps), 664 mode=mode, 665 skip_values_uniqueness_check=True, 666 ) 667 668 def __setitem__( 669 self, 670 element: Union[ 671 'Box', 672 'Polygon', 673 'Mask', 674 'ScoreMap', 675 ], 676 config: Union[ 677 'Image', 678 np.ndarray, 679 Tuple[int, ...], 680 int, 681 ImageSetItemConfig, 682 ], 683 ): # yapf: disable 684 if not isinstance(config, ImageSetItemConfig): 685 value = config 686 alpha = 1.0 687 else: 688 assert isinstance(config, ImageSetItemConfig) 689 value = config.value 690 alpha = config.alpha 691 692 if isinstance(value, tuple): 693 assert value 694 assert isinstance(value[0], int) 695 else: 696 assert not isinstance(value, abc.Iterable) 697 698 # Type inference cannot handle this case. 699 value = cast( 700 Union[ 701 'Image', 702 np.ndarray, 703 Tuple[int, ...], 704 int, 705 ], 706 value 707 ) # yapf: disable 708 assert not isinstance(alpha, abc.Iterable) 709 710 if isinstance(element, ScoreMap): 711 element.fill_image(image=self, value=value) 712 else: 713 element.fill_image(image=self, value=value, alpha=alpha) 714 715 def __getitem__( 716 self, 717 element: Union[ 718 'Box', 719 'Polygon', 720 'Mask', 721 ], 722 ): # yapf: disable 723 return element.extract_image(self) 724 725 def to_box_attached(self, box: 'Box'): 726 assert self.height == box.height 727 assert self.width == box.width 728 return attrs.evolve(self, box=box) 729 730 def to_box_detached(self): 731 assert self.box 732 return attrs.evolve(self, box=None) 733 734 def to_gcn_image( 735 self, 736 lamb: float = 0, 737 eps: float = 1E-8, 738 scale: float = 1.0, 739 ): 740 # Global contrast normalization. 741 # https://cedar.buffalo.edu/~srihari/CSE676/12.2%20Computer%20Vision.pdf 742 # (H, W) or (H, W, 3) 743 mode = self.mode.to_gcn_mode() 744 745 mat = self.mat.astype(np.float32) 746 747 # Normalize mean(contrast). 748 mean = np.mean(mat) 749 mat -= mean 750 751 # Std normalized. 752 std = np.sqrt(lamb + np.mean(mat**2)) 753 mat /= max(eps, std) 754 if scale != 1.0: 755 mat *= scale 756 757 return Image(mat=mat, mode=mode) 758 759 def to_non_gcn_image(self): 760 mode = self.mode.to_non_gcn_mode() 761 762 assert self.mat.dtype == np.float32 763 val_min = np.min(self.mat) 764 mat = self.mat - val_min 765 gap = np.max(mat) 766 mat = mat / gap * 255.0 767 mat = np.round(mat) 768 mat = np.clip(mat, 0, 255).astype(np.uint8) 769 770 return Image(mat=mat, mode=mode) # type: ignore 771 772 def to_target_mode_image(self, target_mode: ImageMode): 773 if target_mode == self.mode: 774 # Identity. 775 return self 776 777 skip_copy = False 778 if self.mode.in_gcn_mode(): 779 self = self.to_non_gcn_image() 780 skip_copy = True 781 782 if self.mode == target_mode: 783 # GCN to non-GCN conversion. 784 return self if skip_copy else self.copy() 785 786 mat = self.mat 787 788 # Pre-slicing. 789 if self.mode in _IMAGE_SRC_MODE_TO_PRE_SLICE: 790 mat: np.ndarray = mat[:, :, _IMAGE_SRC_MODE_TO_PRE_SLICE[self.mode]] 791 792 if (self.mode, target_mode) in _IMAGE_SRC_DST_MODE_TO_CV_CODE: 793 # Shortcut. 794 cv_code = _IMAGE_SRC_DST_MODE_TO_CV_CODE[(self.mode, target_mode)] 795 dst_mat: np.ndarray = cv.cvtColor(mat, cv_code) 796 return Image(mat=dst_mat, mode=target_mode) 797 798 dst_mat = mat 799 if self.mode != ImageMode.RGB: 800 # Convert to RGB. 801 dst_mat: np.ndarray = cv.cvtColor(mat, _IMAGE_SRC_MODE_TO_RGB_CV_CODE[self.mode]) 802 803 if target_mode == ImageMode.RGB: 804 # No need to continue. 805 return Image(mat=dst_mat, mode=ImageMode.RGB) 806 807 # Convert RGB to target mode. 808 assert target_mode in _IMAGE_INV_DST_MODE_TO_RGB_CV_CODE 809 dst_mat: np.ndarray = cv.cvtColor(dst_mat, _IMAGE_INV_DST_MODE_TO_RGB_CV_CODE[target_mode]) 810 811 # Post-slicing. 812 if target_mode in _IMAGE_DST_MODE_TO_POST_SLICE: 813 dst_mat: np.ndarray = dst_mat[:, :, _IMAGE_DST_MODE_TO_POST_SLICE[target_mode]] 814 815 return Image(mat=dst_mat, mode=target_mode) 816 817 def to_grayscale_image(self): 818 return self.to_target_mode_image(ImageMode.GRAYSCALE) 819 820 def to_rgb_image(self): 821 return self.to_target_mode_image(ImageMode.RGB) 822 823 def to_rgba_image(self): 824 return self.to_target_mode_image(ImageMode.RGBA) 825 826 def to_hsv_image(self): 827 return self.to_target_mode_image(ImageMode.HSV) 828 829 def to_hsl_image(self): 830 return self.to_target_mode_image(ImageMode.HSL) 831 832 def to_shifted_image(self, offset_y: int = 0, offset_x: int = 0): 833 assert self.box 834 shifted_box = self.box.to_shifted_box(offset_y=offset_y, offset_x=offset_x) 835 return attrs.evolve(self, box=shifted_box) 836 837 def to_resized_image( 838 self, 839 resized_height: Optional[int] = None, 840 resized_width: Optional[int] = None, 841 cv_resize_interpolation: int = cv.INTER_CUBIC, 842 ): 843 _, _, resized_height, resized_width = generate_shape_and_resized_shape( 844 shapable_or_shape=self, 845 resized_height=resized_height, 846 resized_width=resized_width, 847 ) 848 mat = cv.resize( 849 self.mat, 850 (resized_width, resized_height), 851 interpolation=cv_resize_interpolation, 852 ) 853 return attrs.evolve(self, mat=mat) 854 855 def to_conducted_resized_image( 856 self, 857 shapable_or_shape: Union[Shapable, Tuple[int, int]], 858 resized_height: Optional[int] = None, 859 resized_width: Optional[int] = None, 860 cv_resize_interpolation: int = cv.INTER_CUBIC, 861 ): 862 assert self.box 863 resized_box = self.box.to_conducted_resized_box( 864 shapable_or_shape=shapable_or_shape, 865 resized_height=resized_height, 866 resized_width=resized_width, 867 ) 868 resized_image = self.to_box_detached().to_resized_image( 869 resized_height=resized_box.height, 870 resized_width=resized_box.width, 871 cv_resize_interpolation=cv_resize_interpolation, 872 ) 873 resized_image = resized_image.to_box_attached(resized_box) 874 return resized_image 875 876 def to_cropped_image( 877 self, 878 up: Optional[int] = None, 879 down: Optional[int] = None, 880 left: Optional[int] = None, 881 right: Optional[int] = None, 882 ): 883 assert not self.box 884 885 up = up or 0 886 down = down or self.height - 1 887 left = left or 0 888 right = right or self.width - 1 889 890 return attrs.evolve(self, mat=self.mat[up:down + 1, left:right + 1])
Image( mat: numpy.ndarray, mode: vkit.element.image.ImageMode = <ImageMode.NONE: 'none'>, box: Union[vkit.element.box.Box, NoneType] = None)
2def __init__(self, mat, mode=attr_dict['mode'].default, box=attr_dict['box'].default): 3 _setattr = _cached_setattr_get(self) 4 _setattr('mat', mat) 5 _setattr('mode', mode) 6 _setattr('box', box) 7 self.__attrs_post_init__()
Method generated by attrs for class Image.
@classmethod
def
from_shape( cls, shape: Tuple[int, int], num_channels: int = 3, value: Union[Tuple[int, ...], int] = 255):
264 @classmethod 265 def from_shape( 266 cls, 267 shape: Tuple[int, int], 268 num_channels: int = 3, 269 value: Union[Tuple[int, ...], int] = 255, 270 ): 271 height, width = shape 272 273 if num_channels == 0: 274 mat_shape = (height, width) 275 276 else: 277 assert num_channels > 0 278 279 if isinstance(value, tuple): 280 assert len(value) == num_channels 281 282 mat_shape = (height, width, num_channels) 283 284 mat = np.full(mat_shape, fill_value=value, dtype=np.uint8) 285 return cls(mat=mat)
@classmethod
def
from_shapable( cls, shapable: vkit.element.type.Shapable, num_channels: int = 3, value: Union[Tuple[int, ...], int] = 255):
@classmethod
def
from_file( cls, path: Union[str, os.PathLike], disable_exif_orientation: bool = False):
335 @classmethod 336 def from_file(cls, path: PathType, disable_exif_orientation: bool = False): 337 # NOTE: PilImage.open cannot handle `~`. 338 path = io.file(path).expanduser() 339 340 pil_image = PilImage.open(str(path)) 341 pil_image.load() 342 343 if not disable_exif_orientation: 344 # https://exiftool.org/TagNames/EXIF.html 345 # https://github.com/python-pillow/Pillow/blob/main/src/PIL/ImageOps.py#L571 346 # Avoid unnecessary copy. 347 if pil_image.getexif().get(0x0112): 348 pil_image = PilImageOps.exif_transpose(pil_image) 349 350 return cls.from_pil_image(pil_image)
@classmethod
def
check_values_and_alphas_uniqueness( cls, values: collections.abc.Sequence[typing.Union[vkit.element.image.Image, numpy.ndarray, typing.Tuple[int, ...], int, float]], alphas: collections.abc.Sequence[typing.Union[vkit.element.score_map.ScoreMap, numpy.ndarray, float]]):
@classmethod
def
unpack_element_value_tuples( cls, element_value_tuples: collections.abc.Iterable[typing.Union[tuple[~_E, typing.Union[vkit.element.image.Image, numpy.ndarray, typing.Tuple[int, ...], int]], tuple[~_E, typing.Union[vkit.element.image.Image, numpy.ndarray, typing.Tuple[int, ...], int], typing.Union[float, numpy.ndarray]]]]):
380 @classmethod 381 def unpack_element_value_tuples( 382 cls, 383 element_value_tuples: Iterable[ 384 Union[ 385 Tuple[ 386 _E, 387 Union['Image', np.ndarray, Tuple[int, ...], int], 388 ], 389 Tuple[ 390 _E, 391 Union['Image', np.ndarray, Tuple[int, ...], int], 392 Union[float, np.ndarray], 393 ], 394 ] 395 ], 396 ): # yapf: disable 397 elements: List[_E] = [] 398 values: List[Union[Image, np.ndarray, Tuple[int, ...], int]] = [] 399 alphas: List[Union[float, np.ndarray]] = [] 400 401 for element_value_tuple in element_value_tuples: 402 if len(element_value_tuple) == 2: 403 element, value = element_value_tuple 404 alpha = 1.0 405 else: 406 element, value, alpha = element_value_tuple 407 elements.append(element) 408 values.append(value) 409 alphas.append(alpha) 410 411 return elements, values, alphas
def
fill_by_box_value_tuples( self, box_value_tuples: collections.abc.Iterable[typing.Union[tuple[vkit.element.box.Box, typing.Union[vkit.element.image.Image, numpy.ndarray, typing.Tuple[int, ...], int]], tuple[vkit.element.box.Box, typing.Union[vkit.element.image.Image, numpy.ndarray, typing.Tuple[int, ...], int], typing.Union[float, numpy.ndarray]]]], mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>, skip_values_uniqueness_check: bool = False):
413 def fill_by_box_value_tuples( 414 self, 415 box_value_tuples: Iterable[ 416 Union[ 417 Tuple[ 418 'Box', 419 Union['Image', np.ndarray, Tuple[int, ...], int], 420 ], 421 Tuple[ 422 'Box', 423 Union['Image', np.ndarray, Tuple[int, ...], int], 424 Union[float, np.ndarray], 425 ], 426 ] 427 ], 428 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 429 skip_values_uniqueness_check: bool = False, 430 ): # yapf: disable 431 boxes, values, alphas = self.unpack_element_value_tuples(box_value_tuples) 432 433 boxes_mask = generate_fill_by_boxes_mask(self.shape, boxes, mode) 434 if boxes_mask is None: 435 for box, value, alpha in zip(boxes, values, alphas): 436 box.fill_image( 437 image=self, 438 value=value, 439 alpha=alpha, 440 ) 441 442 else: 443 unique = True 444 if not skip_values_uniqueness_check: 445 unique = self.check_values_and_alphas_uniqueness(values, alphas) 446 447 if unique: 448 boxes_mask.fill_image( 449 image=self, 450 value=values[0], 451 alpha=alphas[0], 452 ) 453 else: 454 for box, value, alpha in zip(boxes, values, alphas): 455 box_mask = box.extract_mask(boxes_mask).to_box_attached(box) 456 box_mask.fill_image( 457 image=self, 458 value=value, 459 alpha=alpha, 460 )
def
fill_by_boxes( self, boxes: collections.abc.Iterable[vkit.element.box.Box], value: Union[vkit.element.image.Image, numpy.ndarray, Tuple[int, ...], int], alpha: Union[numpy.ndarray, float] = 1.0, mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>):
462 def fill_by_boxes( 463 self, 464 boxes: Iterable['Box'], 465 value: Union['Image', np.ndarray, Tuple[int, ...], int], 466 alpha: Union[np.ndarray, float] = 1.0, 467 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 468 ): 469 self.fill_by_box_value_tuples( 470 box_value_tuples=((box, value, alpha) for box in boxes), 471 mode=mode, 472 skip_values_uniqueness_check=True, 473 )
def
fill_by_polygon_value_tuples( self, polygon_value_tuples: collections.abc.Iterable[typing.Union[tuple[vkit.element.polygon.Polygon, typing.Union[vkit.element.image.Image, numpy.ndarray, typing.Tuple[int, ...], int]], tuple[vkit.element.polygon.Polygon, typing.Union[vkit.element.image.Image, numpy.ndarray, typing.Tuple[int, ...], int], typing.Union[float, numpy.ndarray]]]], mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>, skip_values_uniqueness_check: bool = False):
475 def fill_by_polygon_value_tuples( 476 self, 477 polygon_value_tuples: Iterable[ 478 Union[ 479 Tuple[ 480 'Polygon', 481 Union['Image', np.ndarray, Tuple[int, ...], int], 482 ], 483 Tuple[ 484 'Polygon', 485 Union['Image', np.ndarray, Tuple[int, ...], int], 486 Union[float, np.ndarray], 487 ], 488 ] 489 ], 490 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 491 skip_values_uniqueness_check: bool = False, 492 ): # yapf: disable 493 polygons, values, alphas = self.unpack_element_value_tuples(polygon_value_tuples) 494 495 polygons_mask = generate_fill_by_polygons_mask(self.shape, polygons, mode) 496 if polygons_mask is None: 497 for polygon, value, alpha in zip(polygons, values, alphas): 498 polygon.fill_image( 499 image=self, 500 value=value, 501 alpha=alpha, 502 ) 503 504 else: 505 unique = True 506 if not skip_values_uniqueness_check: 507 unique = self.check_values_and_alphas_uniqueness(values, alphas) 508 509 if unique: 510 polygons_mask.fill_image( 511 image=self, 512 value=values[0], 513 alpha=alphas[0], 514 ) 515 else: 516 for polygon, value, alpha in zip(polygons, values, alphas): 517 bounding_box = polygon.to_bounding_box() 518 polygon_mask = bounding_box.extract_mask(polygons_mask) 519 polygon_mask = polygon_mask.to_box_attached(bounding_box) 520 polygon_mask.fill_image( 521 image=self, 522 value=value, 523 alpha=alpha, 524 )
def
fill_by_polygons( self, polygons: collections.abc.Iterable[vkit.element.polygon.Polygon], value: Union[vkit.element.image.Image, numpy.ndarray, Tuple[int, ...], int], alpha: Union[numpy.ndarray, float] = 1.0, mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>):
526 def fill_by_polygons( 527 self, 528 polygons: Iterable['Polygon'], 529 value: Union['Image', np.ndarray, Tuple[int, ...], int], 530 alpha: Union[np.ndarray, float] = 1.0, 531 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 532 ): 533 self.fill_by_polygon_value_tuples( 534 polygon_value_tuples=((polygon, value, alpha) for polygon in polygons), 535 mode=mode, 536 skip_values_uniqueness_check=True, 537 )
def
fill_by_mask_value_tuples( self, mask_value_tuples: collections.abc.Iterable[typing.Union[tuple[vkit.element.mask.Mask, typing.Union[vkit.element.image.Image, numpy.ndarray, typing.Tuple[int, ...], int]], tuple[vkit.element.mask.Mask, typing.Union[vkit.element.image.Image, numpy.ndarray, typing.Tuple[int, ...], int], typing.Union[float, numpy.ndarray]]]], mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>, skip_values_uniqueness_check: bool = False):
539 def fill_by_mask_value_tuples( 540 self, 541 mask_value_tuples: Iterable[ 542 Union[ 543 Tuple[ 544 'Mask', 545 Union['Image', np.ndarray, Tuple[int, ...], int], 546 ], 547 Tuple[ 548 'Mask', 549 Union['Image', np.ndarray, Tuple[int, ...], int], 550 Union[float, np.ndarray], 551 ], 552 ] 553 ], 554 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 555 skip_values_uniqueness_check: bool = False, 556 ): # yapf: disable 557 masks, values, alphas = self.unpack_element_value_tuples(mask_value_tuples) 558 559 masks_mask = generate_fill_by_masks_mask(self.shape, masks, mode) 560 if masks_mask is None: 561 for mask, value, alpha in zip(masks, values, alphas): 562 mask.fill_image( 563 image=self, 564 value=value, 565 alpha=alpha, 566 ) 567 568 else: 569 unique = True 570 if not skip_values_uniqueness_check: 571 unique = self.check_values_and_alphas_uniqueness(values, alphas) 572 573 if unique: 574 masks_mask.fill_image( 575 image=self, 576 value=values[0], 577 alpha=alphas[0], 578 ) 579 else: 580 for mask, value, alpha in zip(masks, values, alphas): 581 if mask.box: 582 boxed_mask = mask.box.extract_mask(masks_mask) 583 else: 584 boxed_mask = masks_mask 585 586 boxed_mask = boxed_mask.copy() 587 mask.to_inverted_mask().fill_mask(boxed_mask, value=0) 588 boxed_mask.fill_image( 589 image=self, 590 value=value, 591 alpha=alpha, 592 )
def
fill_by_masks( self, masks: collections.abc.Iterable[vkit.element.mask.Mask], value: Union[vkit.element.image.Image, numpy.ndarray, Tuple[int, ...], int], alpha: Union[numpy.ndarray, float] = 1.0, mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>):
594 def fill_by_masks( 595 self, 596 masks: Iterable['Mask'], 597 value: Union['Image', np.ndarray, Tuple[int, ...], int], 598 alpha: Union[np.ndarray, float] = 1.0, 599 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 600 ): 601 self.fill_by_mask_value_tuples( 602 mask_value_tuples=((mask, value, alpha) for mask in masks), 603 mode=mode, 604 skip_values_uniqueness_check=True, 605 )
def
fill_by_score_map_value_tuples( self, score_map_value_tuples: collections.abc.Iterable[tuple[vkit.element.score_map.ScoreMap, typing.Union[vkit.element.image.Image, numpy.ndarray, typing.Tuple[int, ...], int]]], mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>, skip_values_uniqueness_check: bool = False):
607 def fill_by_score_map_value_tuples( 608 self, 609 score_map_value_tuples: Iterable[ 610 Tuple[ 611 'ScoreMap', 612 Union['Image', np.ndarray, Tuple[int, ...], int], 613 ] 614 ], 615 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 616 skip_values_uniqueness_check: bool = False, 617 ): # yapf: disable 618 # NOTE: score maps serve as masks & alphas, hence ignoring unpacked alphas. 619 score_maps, values, _ = self.unpack_element_value_tuples(score_map_value_tuples) 620 621 score_maps_mask = generate_fill_by_score_maps_mask(self.shape, score_maps, mode) 622 if score_maps_mask is None: 623 for score_map, value in zip(score_maps, values): 624 score_map.fill_image( 625 image=self, 626 value=value, 627 ) 628 629 else: 630 unique = True 631 if not skip_values_uniqueness_check: 632 unique = check_elements_uniqueness(values) 633 634 if unique: 635 # This is unlikely to happen. 636 score_maps_mask.fill_image( 637 image=self, 638 value=values[0], 639 alpha=score_maps[0], 640 ) 641 else: 642 for score_map, value in zip(score_maps, values): 643 if score_map.box: 644 boxed_mask = score_map.box.extract_mask(score_maps_mask) 645 else: 646 boxed_mask = score_maps_mask 647 648 boxed_mask = boxed_mask.copy() 649 score_map.to_mask().to_inverted_mask().fill_mask(boxed_mask, value=0) 650 boxed_mask.fill_image( 651 image=self, 652 value=value, 653 alpha=score_map, 654 )
def
fill_by_score_maps( self, score_maps: collections.abc.Iterable[vkit.element.score_map.ScoreMap], value: Union[vkit.element.image.Image, numpy.ndarray, Tuple[int, ...], int], mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>):
656 def fill_by_score_maps( 657 self, 658 score_maps: Iterable['ScoreMap'], 659 value: Union['Image', np.ndarray, Tuple[int, ...], int], 660 mode: ElementSetOperationMode = ElementSetOperationMode.UNION, 661 ): 662 self.fill_by_score_map_value_tuples( 663 score_map_value_tuples=((score_map, value) for score_map in score_maps), 664 mode=mode, 665 skip_values_uniqueness_check=True, 666 )
def
to_gcn_image(self, lamb: float = 0, eps: float = 1e-08, scale: float = 1.0):
734 def to_gcn_image( 735 self, 736 lamb: float = 0, 737 eps: float = 1E-8, 738 scale: float = 1.0, 739 ): 740 # Global contrast normalization. 741 # https://cedar.buffalo.edu/~srihari/CSE676/12.2%20Computer%20Vision.pdf 742 # (H, W) or (H, W, 3) 743 mode = self.mode.to_gcn_mode() 744 745 mat = self.mat.astype(np.float32) 746 747 # Normalize mean(contrast). 748 mean = np.mean(mat) 749 mat -= mean 750 751 # Std normalized. 752 std = np.sqrt(lamb + np.mean(mat**2)) 753 mat /= max(eps, std) 754 if scale != 1.0: 755 mat *= scale 756 757 return Image(mat=mat, mode=mode)
def
to_non_gcn_image(self):
759 def to_non_gcn_image(self): 760 mode = self.mode.to_non_gcn_mode() 761 762 assert self.mat.dtype == np.float32 763 val_min = np.min(self.mat) 764 mat = self.mat - val_min 765 gap = np.max(mat) 766 mat = mat / gap * 255.0 767 mat = np.round(mat) 768 mat = np.clip(mat, 0, 255).astype(np.uint8) 769 770 return Image(mat=mat, mode=mode) # type: ignore
772 def to_target_mode_image(self, target_mode: ImageMode): 773 if target_mode == self.mode: 774 # Identity. 775 return self 776 777 skip_copy = False 778 if self.mode.in_gcn_mode(): 779 self = self.to_non_gcn_image() 780 skip_copy = True 781 782 if self.mode == target_mode: 783 # GCN to non-GCN conversion. 784 return self if skip_copy else self.copy() 785 786 mat = self.mat 787 788 # Pre-slicing. 789 if self.mode in _IMAGE_SRC_MODE_TO_PRE_SLICE: 790 mat: np.ndarray = mat[:, :, _IMAGE_SRC_MODE_TO_PRE_SLICE[self.mode]] 791 792 if (self.mode, target_mode) in _IMAGE_SRC_DST_MODE_TO_CV_CODE: 793 # Shortcut. 794 cv_code = _IMAGE_SRC_DST_MODE_TO_CV_CODE[(self.mode, target_mode)] 795 dst_mat: np.ndarray = cv.cvtColor(mat, cv_code) 796 return Image(mat=dst_mat, mode=target_mode) 797 798 dst_mat = mat 799 if self.mode != ImageMode.RGB: 800 # Convert to RGB. 801 dst_mat: np.ndarray = cv.cvtColor(mat, _IMAGE_SRC_MODE_TO_RGB_CV_CODE[self.mode]) 802 803 if target_mode == ImageMode.RGB: 804 # No need to continue. 805 return Image(mat=dst_mat, mode=ImageMode.RGB) 806 807 # Convert RGB to target mode. 808 assert target_mode in _IMAGE_INV_DST_MODE_TO_RGB_CV_CODE 809 dst_mat: np.ndarray = cv.cvtColor(dst_mat, _IMAGE_INV_DST_MODE_TO_RGB_CV_CODE[target_mode]) 810 811 # Post-slicing. 812 if target_mode in _IMAGE_DST_MODE_TO_POST_SLICE: 813 dst_mat: np.ndarray = dst_mat[:, :, _IMAGE_DST_MODE_TO_POST_SLICE[target_mode]] 814 815 return Image(mat=dst_mat, mode=target_mode)
def
to_resized_image( self, resized_height: Union[int, NoneType] = None, resized_width: Union[int, NoneType] = None, cv_resize_interpolation: int = 2):
837 def to_resized_image( 838 self, 839 resized_height: Optional[int] = None, 840 resized_width: Optional[int] = None, 841 cv_resize_interpolation: int = cv.INTER_CUBIC, 842 ): 843 _, _, resized_height, resized_width = generate_shape_and_resized_shape( 844 shapable_or_shape=self, 845 resized_height=resized_height, 846 resized_width=resized_width, 847 ) 848 mat = cv.resize( 849 self.mat, 850 (resized_width, resized_height), 851 interpolation=cv_resize_interpolation, 852 ) 853 return attrs.evolve(self, mat=mat)
def
to_conducted_resized_image( self, shapable_or_shape: Union[vkit.element.type.Shapable, Tuple[int, int]], resized_height: Union[int, NoneType] = None, resized_width: Union[int, NoneType] = None, cv_resize_interpolation: int = 2):
855 def to_conducted_resized_image( 856 self, 857 shapable_or_shape: Union[Shapable, Tuple[int, int]], 858 resized_height: Optional[int] = None, 859 resized_width: Optional[int] = None, 860 cv_resize_interpolation: int = cv.INTER_CUBIC, 861 ): 862 assert self.box 863 resized_box = self.box.to_conducted_resized_box( 864 shapable_or_shape=shapable_or_shape, 865 resized_height=resized_height, 866 resized_width=resized_width, 867 ) 868 resized_image = self.to_box_detached().to_resized_image( 869 resized_height=resized_box.height, 870 resized_width=resized_box.width, 871 cv_resize_interpolation=cv_resize_interpolation, 872 ) 873 resized_image = resized_image.to_box_attached(resized_box) 874 return resized_image
def
to_cropped_image( self, up: Union[int, NoneType] = None, down: Union[int, NoneType] = None, left: Union[int, NoneType] = None, right: Union[int, NoneType] = None):
876 def to_cropped_image( 877 self, 878 up: Optional[int] = None, 879 down: Optional[int] = None, 880 left: Optional[int] = None, 881 right: Optional[int] = None, 882 ): 883 assert not self.box 884 885 up = up or 0 886 down = down or self.height - 1 887 left = left or 0 888 right = right or self.width - 1 889 890 return attrs.evolve(self, mat=self.mat[up:down + 1, left:right + 1])