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.

def to_ndim(self):
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()
def to_dtype(self):
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()
def to_num_channels(self):
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
def supports_gcn_mode(self):
73    def supports_gcn_mode(self):
74        return self not in _IMAGE_MODE_NON_GCN_TO_GCN
def to_gcn_mode(self):
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]
def in_gcn_mode(self):
81    def in_gcn_mode(self):
82        return self in _IMAGE_MODE_GCN_TO_NON_GCN
def to_non_gcn_mode(self):
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]
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)
161    def __init__(self, image: 'Image'):
162        super().__init__()
163        self.image = image
class Image(vkit.element.type.Shapable):
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(self, 'mat', mat)
4    _setattr(self, 'mode', mode)
5    _setattr(self, 'box', box)
6    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):
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        )
@classmethod
def from_pil_image(cls, pil_image: PIL.Image.Image):
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)
def to_pil_image(self):
332    def to_pil_image(self):
333        return PilImage.fromarray(self.mat)
@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)
def to_file( self, path: Union[str, os.PathLike], disable_to_rgb_image: bool = False):
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))
def copy(self):
365    def copy(self):
366        return attrs.evolve(self, mat=self.mat.copy())
def assign_mat(self, mat: numpy.ndarray):
368    def assign_mat(self, mat: np.ndarray):
369        with self.writable_context:
370            object.__setattr__(self, 'mat', mat)
@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]]):
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)
@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_box_attached(self, box: vkit.element.box.Box):
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)
def to_box_detached(self):
730    def to_box_detached(self):
731        assert self.box
732        return attrs.evolve(self, box=None)
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
def to_target_mode_image(self, target_mode: vkit.element.image.ImageMode):
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_grayscale_image(self):
817    def to_grayscale_image(self):
818        return self.to_target_mode_image(ImageMode.GRAYSCALE)
def to_rgb_image(self):
820    def to_rgb_image(self):
821        return self.to_target_mode_image(ImageMode.RGB)
def to_rgba_image(self):
823    def to_rgba_image(self):
824        return self.to_target_mode_image(ImageMode.RGBA)
def to_hsv_image(self):
826    def to_hsv_image(self):
827        return self.to_target_mode_image(ImageMode.HSV)
def to_hsl_image(self):
829    def to_hsl_image(self):
830        return self.to_target_mode_image(ImageMode.HSL)
def to_shifted_image(self, offset_y: int = 0, offset_x: int = 0):
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)
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])