vkit.element.mask

  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, Optional, Tuple, Union, List, Iterable, TypeVar, Sequence
 15from contextlib import ContextDecorator
 16import logging
 17
 18import attrs
 19import numpy as np
 20import cv2 as cv
 21from shapely.geometry import (
 22    Polygon as ShapelyPolygon,
 23    MultiPolygon as ShapelyMultiPolygon,
 24    GeometryCollection as ShapelyGeometryCollection,
 25)
 26from shapely.validation import make_valid as shapely_make_valid
 27
 28from vkit.utility import attrs_lazy_field
 29from .type import Shapable, ElementSetOperationMode
 30from .opt import generate_resized_shape
 31
 32logger = logging.getLogger(__name__)
 33
 34
 35@attrs.define
 36class MaskSetItemConfig:
 37    value: Union['Mask', np.ndarray, int] = 1
 38    keep_max_value: bool = False
 39    keep_min_value: bool = False
 40
 41
 42class WritableMaskContextDecorator(ContextDecorator):
 43
 44    def __init__(self, mask: 'Mask'):
 45        super().__init__()
 46        self.mask = mask
 47
 48    def __enter__(self):
 49        if self.mask.mat.flags.c_contiguous:
 50            assert not self.mask.mat.flags.writeable
 51
 52        try:
 53            self.mask.mat.flags.writeable = True
 54        except ValueError:
 55            # Copy on write.
 56            object.__setattr__(
 57                self.mask,
 58                'mat',
 59                np.array(self.mask.mat),
 60            )
 61            assert self.mask.mat.flags.writeable
 62
 63    def __exit__(self, *exc):  # type: ignore
 64        self.mask.mat.flags.writeable = False
 65        self.mask.set_np_mask_out_of_date()
 66
 67
 68_E = TypeVar('_E', 'Box', 'Polygon')
 69
 70
 71@attrs.define(frozen=True, eq=False)
 72class Mask(Shapable):
 73    mat: np.ndarray
 74    box: Optional['Box'] = None
 75
 76    _np_mask: Optional[np.ndarray] = attrs_lazy_field()
 77
 78    def __attrs_post_init__(self):
 79        if self.mat.dtype != np.uint8:
 80            raise RuntimeError('mat.dtype != np.uint8')
 81        if self.mat.ndim != 2:
 82            raise RuntimeError('ndim should == 2.')
 83
 84        # For the control of write.
 85        self.mat.flags.writeable = False
 86
 87        if self.box and self.shape != self.box.shape:
 88            raise RuntimeError('self.shape != box.shape.')
 89
 90    def lazy_post_init_np_mask(self):
 91        if self._np_mask is not None:
 92            return self._np_mask
 93
 94        object.__setattr__(self, '_np_mask', (self.mat > 0))
 95        return cast(np.ndarray, self._np_mask)
 96
 97    ###############
 98    # Constructor #
 99    ###############
100    @classmethod
101    def from_shape(cls, shape: Tuple[int, int], value: int = 0):
102        height, width = shape
103        if value == 0:
104            np_init_func = np.zeros
105        else:
106            assert value == 1
107            np_init_func = np.ones
108        mat = np_init_func((height, width), dtype=np.uint8)
109        return cls(mat=mat)
110
111    @classmethod
112    def from_shapable(cls, shapable: Shapable, value: int = 0):
113        return cls.from_shape(shape=shapable.shape, value=value)
114
115    @classmethod
116    def _unpack_shape_or_box(cls, shape_or_box: Union[Tuple[int, int], 'Box']):
117        if isinstance(shape_or_box, Box):
118            attached_box = shape_or_box
119            shape = attached_box.shape
120        else:
121            attached_box = None
122            shape = shape_or_box
123        return shape, attached_box
124
125    @classmethod
126    def _from_np_active_count(
127        cls,
128        shape: Tuple[int, int],
129        mode: ElementSetOperationMode,
130        np_active_count: np.ndarray,
131        attached_box: Optional['Box'],
132    ):
133        mask = Mask.from_shape(shape)
134
135        with mask.writable_context:
136            if mode == ElementSetOperationMode.UNION:
137                mask.mat[np_active_count > 0] = 1
138
139            elif mode == ElementSetOperationMode.DISTINCT:
140                mask.mat[np_active_count == 1] = 1
141
142            elif mode == ElementSetOperationMode.INTERSECT:
143                mask.mat[np_active_count > 1] = 1
144
145            else:
146                raise NotImplementedError()
147
148        if attached_box:
149            mask = mask.to_box_attached(attached_box)
150
151        return mask
152
153    @classmethod
154    def from_boxes(
155        cls,
156        shape_or_box: Union[Tuple[int, int], 'Box'],
157        boxes: Iterable['Box'],
158        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
159    ):
160        shape, attached_box = cls._unpack_shape_or_box(shape_or_box)
161        np_active_count = np.zeros(shape, dtype=np.int32)
162
163        for box in boxes:
164            if attached_box:
165                box = box.to_relative_box(
166                    origin_y=attached_box.up,
167                    origin_x=attached_box.left,
168                )
169            np_boxed_active_count = box.extract_np_array(np_active_count)
170            np_boxed_active_count += 1
171
172        return cls._from_np_active_count(shape, mode, np_active_count, attached_box)
173
174    @classmethod
175    def from_polygons(
176        cls,
177        shape_or_box: Union[Tuple[int, int], 'Box'],
178        polygons: Iterable['Polygon'],
179        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
180    ):
181        shape, attached_box = cls._unpack_shape_or_box(shape_or_box)
182        np_active_count = np.zeros(shape, dtype=np.int32)
183
184        for polygon in polygons:
185            box = polygon.bounding_box
186            if attached_box:
187                box = box.to_relative_box(
188                    origin_y=attached_box.up,
189                    origin_x=attached_box.left,
190                )
191            np_boxed_active_count = box.extract_np_array(np_active_count)
192            np_boxed_active_count[polygon.internals.np_mask] += 1
193
194        return cls._from_np_active_count(shape, mode, np_active_count, attached_box)
195
196    @classmethod
197    def from_masks(
198        cls,
199        shape_or_box: Union[Tuple[int, int], 'Box'],
200        masks: Iterable['Mask'],
201        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
202    ):
203        shape, attached_box = cls._unpack_shape_or_box(shape_or_box)
204        np_active_count = np.zeros(shape, dtype=np.int32)
205
206        for mask in masks:
207            if mask.box:
208                box = mask.box
209                if attached_box:
210                    box = box.to_relative_box(
211                        origin_y=attached_box.up,
212                        origin_x=attached_box.left,
213                    )
214                np_boxed_active_count = box.extract_np_array(np_active_count)
215            else:
216                np_boxed_active_count = np_active_count
217            np_boxed_active_count[mask.np_mask] += 1
218
219        return cls._from_np_active_count(shape, mode, np_active_count, attached_box)
220
221    @classmethod
222    def from_score_maps(
223        cls,
224        shape_or_box: Union[Tuple[int, int], 'Box'],
225        score_maps: Iterable['ScoreMap'],
226        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
227    ):
228        shape, attached_box = cls._unpack_shape_or_box(shape_or_box)
229        np_active_count = np.zeros(shape, dtype=np.int32)
230
231        for score_map in score_maps:
232            if score_map.box:
233                box = score_map.box
234                if attached_box:
235                    box = box.to_relative_box(
236                        origin_y=attached_box.up,
237                        origin_x=attached_box.left,
238                    )
239                np_boxed_active_count = box.extract_np_array(np_active_count)
240            else:
241                np_boxed_active_count = np_active_count
242            np_boxed_active_count[score_map.to_mask().np_mask] += 1
243
244        return cls._from_np_active_count(shape, mode, np_active_count, attached_box)
245
246    ############
247    # Property #
248    ############
249    @property
250    def height(self):
251        return self.mat.shape[0]
252
253    @property
254    def width(self):
255        return self.mat.shape[1]
256
257    @property
258    def equivalent_box(self):
259        return self.box or Box.from_shapable(self)
260
261    @property
262    def np_mask(self):
263        return self.lazy_post_init_np_mask()
264
265    @property
266    def writable_context(self):
267        return WritableMaskContextDecorator(self)
268
269    ############
270    # Operator #
271    ############
272    def copy(self):
273        return attrs.evolve(self, mat=self.mat.copy())
274
275    def set_np_mask_out_of_date(self):
276        object.__setattr__(self, '_np_mask', None)
277
278    def assign_mat(self, mat: np.ndarray):
279        with self.writable_context:
280            object.__setattr__(self, 'mat', mat)
281
282    @classmethod
283    def unpack_element_value_pairs(
284        cls,
285        element_value_pairs: Iterable[Tuple[_E, Union['Mask', np.ndarray, int]]],
286    ):
287        elements: List[_E] = []
288        values: List[Union[Mask, np.ndarray, int]] = []
289        for element, value in element_value_pairs:
290            elements.append(element)
291            values.append(value)
292        return elements, values
293
294    def fill_by_box_value_pairs(
295        self,
296        box_value_pairs: Iterable[Tuple['Box', Union['Mask', np.ndarray, int]]],
297        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
298        keep_max_value: bool = False,
299        keep_min_value: bool = False,
300        skip_values_uniqueness_check: bool = False,
301    ):
302        boxes, values = self.unpack_element_value_pairs(box_value_pairs)
303
304        boxes_mask = generate_fill_by_boxes_mask(self.shape, boxes, mode)
305        if boxes_mask is None:
306            for box, value in zip(boxes, values):
307                box.fill_mask(
308                    mask=self,
309                    value=value,
310                    keep_max_value=keep_max_value,
311                    keep_min_value=keep_min_value,
312                )
313
314        else:
315            unique = True
316            if not skip_values_uniqueness_check:
317                unique = check_elements_uniqueness(values)
318
319            if unique:
320                boxes_mask.fill_mask(
321                    mask=self,
322                    value=values[0],
323                    keep_max_value=keep_max_value,
324                    keep_min_value=keep_min_value,
325                )
326            else:
327                for box, value in zip(boxes, values):
328                    box_mask = box.extract_mask(boxes_mask).to_box_attached(box)
329                    box_mask.fill_mask(
330                        mask=self,
331                        value=value,
332                        keep_max_value=keep_max_value,
333                        keep_min_value=keep_min_value,
334                    )
335
336    def fill_by_boxes(
337        self,
338        boxes: Iterable['Box'],
339        value: Union['Mask', np.ndarray, int] = 1,
340        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
341        keep_max_value: bool = False,
342        keep_min_value: bool = False,
343    ):
344        self.fill_by_box_value_pairs(
345            box_value_pairs=((box, value) for box in boxes),
346            mode=mode,
347            keep_max_value=keep_max_value,
348            keep_min_value=keep_min_value,
349            skip_values_uniqueness_check=True,
350        )
351
352    def fill_by_polygon_value_pairs(
353        self,
354        polygon_value_pairs: Iterable[Tuple['Polygon', Union['Mask', np.ndarray, int]]],
355        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
356        keep_max_value: bool = False,
357        keep_min_value: bool = False,
358        skip_values_uniqueness_check: bool = False,
359    ):
360        polygons, values = self.unpack_element_value_pairs(polygon_value_pairs)
361
362        polygons_mask = generate_fill_by_polygons_mask(self.shape, polygons, mode)
363        if polygons_mask is None:
364            for polygon, value in zip(polygons, values):
365                polygon.fill_mask(
366                    mask=self,
367                    value=value,
368                    keep_max_value=keep_max_value,
369                    keep_min_value=keep_min_value,
370                )
371
372        else:
373            unique = True
374            if not skip_values_uniqueness_check:
375                unique = check_elements_uniqueness(values)
376
377            if unique:
378                polygons_mask.fill_mask(
379                    mask=self,
380                    value=values[0],
381                    keep_max_value=keep_max_value,
382                    keep_min_value=keep_min_value,
383                )
384            else:
385                for polygon, value in zip(polygons, values):
386                    bounding_box = polygon.to_bounding_box()
387                    polygon_mask = bounding_box.extract_mask(polygons_mask)
388                    polygon_mask = polygon_mask.to_box_attached(bounding_box)
389                    polygon_mask.fill_mask(
390                        mask=self,
391                        value=value,
392                        keep_max_value=keep_max_value,
393                        keep_min_value=keep_min_value,
394                    )
395
396    def fill_by_polygons(
397        self,
398        polygons: Iterable['Polygon'],
399        value: Union['Mask', np.ndarray, int] = 1,
400        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
401        keep_max_value: bool = False,
402        keep_min_value: bool = False,
403    ):
404        self.fill_by_polygon_value_pairs(
405            polygon_value_pairs=((polygon, value) for polygon in polygons),
406            mode=mode,
407            keep_max_value=keep_max_value,
408            keep_min_value=keep_min_value,
409            skip_values_uniqueness_check=True,
410        )
411
412    def __setitem__(
413        self,
414        element: Union['Box', 'Polygon'],
415        config: Union[
416            'Mask',
417            np.ndarray,
418            int,
419            MaskSetItemConfig,
420        ],
421    ):  # yapf: disable
422        if not isinstance(config, MaskSetItemConfig):
423            value = config
424            keep_max_value = False
425            keep_min_value = False
426        else:
427            assert isinstance(config, MaskSetItemConfig)
428            value = config.value
429            keep_max_value = config.keep_max_value
430            keep_min_value = config.keep_min_value
431
432        element.fill_mask(
433            mask=self,
434            value=value,
435            keep_min_value=keep_min_value,
436            keep_max_value=keep_max_value,
437        )
438
439    def __getitem__(
440        self,
441        element: Union['Box', 'Polygon'],
442    ):
443        return element.extract_mask(self)
444
445    def to_inverted_mask(self):
446        mat = (~self.np_mask).astype(np.uint8)
447        return attrs.evolve(self, mat=mat)
448
449    def to_shifted_mask(self, offset_y: int = 0, offset_x: int = 0):
450        assert self.box
451        shifted_box = self.box.to_shifted_box(offset_y=offset_y, offset_x=offset_x)
452        return attrs.evolve(self, box=shifted_box)
453
454    def to_resized_mask(
455        self,
456        resized_height: Optional[int] = None,
457        resized_width: Optional[int] = None,
458        cv_resize_interpolation: int = cv.INTER_CUBIC,
459        binarization_threshold: int = 0,
460    ):
461        assert not self.box
462        resized_height, resized_width = generate_resized_shape(
463            height=self.height,
464            width=self.width,
465            resized_height=resized_height,
466            resized_width=resized_width,
467        )
468
469        # Deal with precision loss.
470        mat = self.np_mask.astype(np.uint8) * 255
471        mat = cv.resize(
472            mat,
473            (resized_width, resized_height),
474            interpolation=cv_resize_interpolation,
475        )
476        mat = cast(np.ndarray, mat)
477        mat = (mat > binarization_threshold).astype(np.uint8)
478
479        return Mask(mat=mat)
480
481    def to_conducted_resized_mask(
482        self,
483        shapable_or_shape: Union[Shapable, Tuple[int, int]],
484        resized_height: Optional[int] = None,
485        resized_width: Optional[int] = None,
486        cv_resize_interpolation: int = cv.INTER_CUBIC,
487        binarization_threshold: int = 0,
488    ):
489        assert self.box
490        resized_box = self.box.to_conducted_resized_box(
491            shapable_or_shape=shapable_or_shape,
492            resized_height=resized_height,
493            resized_width=resized_width,
494        )
495        resized_mask = self.to_box_detached().to_resized_mask(
496            resized_height=resized_box.height,
497            resized_width=resized_box.width,
498            cv_resize_interpolation=cv_resize_interpolation,
499            binarization_threshold=binarization_threshold,
500        )
501        resized_mask = resized_mask.to_box_attached(resized_box)
502        return resized_mask
503
504    def to_cropped_mask(
505        self,
506        up: Optional[int] = None,
507        down: Optional[int] = None,
508        left: Optional[int] = None,
509        right: Optional[int] = None,
510    ):
511        assert not self.box
512
513        up = up or 0
514        down = down or self.height - 1
515        left = left or 0
516        right = right or self.width - 1
517
518        return attrs.evolve(self, mat=self.mat[up:down + 1, left:right + 1])
519
520    def to_box_attached(self, box: 'Box'):
521        assert self.height == box.height
522        assert self.width == box.width
523        return attrs.evolve(self, box=box)
524
525    def to_box_detached(self):
526        assert self.box
527        return attrs.evolve(self, box=None)
528
529    def fill_np_array(
530        self,
531        mat: np.ndarray,
532        value: Union[np.ndarray, Tuple[float, ...], float],
533        alpha: Union['ScoreMap', np.ndarray, float] = 1.0,
534        keep_max_value: bool = False,
535        keep_min_value: bool = False,
536    ):
537        self.equivalent_box.fill_np_array(
538            mat=mat,
539            value=value,
540            np_mask=self.np_mask,
541            alpha=alpha,
542            keep_max_value=keep_max_value,
543            keep_min_value=keep_min_value,
544        )
545
546    def extract_mask(self, mask: 'Mask'):
547        mask = self.equivalent_box.extract_mask(mask)
548
549        mask = mask.copy()
550        self.to_inverted_mask().fill_mask(mask, value=0)
551        return mask
552
553    def fill_mask(
554        self,
555        mask: 'Mask',
556        value: Union['Mask', np.ndarray, int] = 1,
557        keep_max_value: bool = False,
558        keep_min_value: bool = False,
559    ):
560        self.equivalent_box.fill_mask(
561            mask=mask,
562            value=value,
563            mask_mask=self,
564            keep_max_value=keep_max_value,
565            keep_min_value=keep_min_value,
566        )
567
568    def extract_score_map(self, score_map: 'ScoreMap'):
569        score_map = self.equivalent_box.extract_score_map(score_map)
570
571        score_map = score_map.copy()
572        self.to_inverted_mask().fill_score_map(score_map, value=0.0)
573        return score_map
574
575    def fill_score_map(
576        self,
577        score_map: 'ScoreMap',
578        value: Union['ScoreMap', np.ndarray, float],
579        keep_max_value: bool = False,
580        keep_min_value: bool = False,
581    ):
582        self.equivalent_box.fill_score_map(
583            score_map=score_map,
584            value=value,
585            score_map_mask=self,
586            keep_max_value=keep_max_value,
587            keep_min_value=keep_min_value,
588        )
589
590    def to_score_map(self):
591        mat = self.np_mask.astype(np.float32)
592        return ScoreMap(mat=mat, box=self.box)
593
594    def extract_image(self, image: 'Image'):
595        image = self.equivalent_box.extract_image(image)
596
597        image = image.copy()
598        self.to_inverted_mask().fill_image(image, value=0)
599        return image
600
601    def fill_image(
602        self,
603        image: 'Image',
604        value: Union['Image', np.ndarray, Tuple[int, ...], int],
605        alpha: Union['ScoreMap', np.ndarray, float] = 1.0,
606    ):
607        self.equivalent_box.fill_image(
608            image=image,
609            value=value,
610            image_mask=self,
611            alpha=alpha,
612        )
613
614    def to_external_box(self):
615        np_mask = self.np_mask
616
617        np_vert_max: np.ndarray = np.amax(np_mask, axis=1)
618        np_vert_nonzero = np.nonzero(np_vert_max)[0]
619        if len(np_vert_nonzero) == 0:
620            raise RuntimeError('to_external_box: empty np_mask.')
621
622        up = int(np_vert_nonzero[0])
623        down = int(np_vert_nonzero[-1])
624
625        np_hori_max: np.ndarray = np.amax(np_mask, axis=0)
626        np_hori_nonzero = np.nonzero(np_hori_max)[0]
627        if len(np_hori_nonzero) == 0:
628            raise RuntimeError('to_external_box: empty np_mask.')
629
630        left = int(np_hori_nonzero[0])
631        right = int(np_hori_nonzero[-1])
632
633        return Box(up=up, down=down, left=left, right=right)
634
635    def to_external_polygon(
636        self,
637        cv_find_contours_method: int = cv.CHAIN_APPROX_SIMPLE,
638    ):
639        polygons = self.to_disconnected_polygons(cv_find_contours_method=cv_find_contours_method)
640        if not polygons:
641            raise RuntimeError('Cannot find any contour.')
642        elif len(polygons) > 1:
643            logger.warning(
644                'More than one polygons is detected, keep the largest one as the external polygon.'
645            )
646            area_max = 0
647            best_polygon = None
648            for polygon in polygons:
649                if polygon.area > area_max:
650                    area_max = polygon.area
651                    best_polygon = polygon
652            assert best_polygon
653            return best_polygon
654        else:
655            return polygons[0]
656
657    def to_disconnected_polygons(
658        self,
659        cv_find_contours_method: int = cv.CHAIN_APPROX_SIMPLE,
660    ) -> Sequence['Polygon']:
661        # [ (N, 1, 2), ... ]
662        # https://stackoverflow.com/a/8830981
663        # https://docs.opencv.org/4.x/d9/d8b/tutorial_py_contours_hierarchy.html
664        cv_contours, cv_hierarchy = cv.findContours(
665            (self.np_mask.astype(np.uint8) * 255),
666            cv.RETR_TREE,
667            cv_find_contours_method,
668        )
669        if not cv_contours:
670            return []
671
672        assert len(cv_hierarchy) == 1
673        assert len(cv_contours) == len(cv_hierarchy[0])
674        cv_hierarchy = cv_hierarchy[0]
675
676        polygons: List[Polygon] = []
677
678        # Ignore logging of shapely.geos.
679        shapely_geos_logger = logging.getLogger('shapely.geos')
680        shapely_geos_logger_level = shapely_geos_logger.level
681        shapely_geos_logger.setLevel(logging.WARNING)
682
683        for cv_contour, cv_contour_hierarchy in zip(cv_contours, cv_hierarchy):
684            assert len(cv_contour_hierarchy) == 4
685            cv_contour_parent = cv_contour_hierarchy[-1]
686            if cv_contour_parent >= 0:
687                continue
688
689            assert cv_contour.shape[1] == 1
690            np_points = np.squeeze(cv_contour, axis=1)
691
692            if self.box:
693                np_points[:, 0] += self.box.left
694                np_points[:, 1] += self.box.up
695
696            if np_points.shape[0] < 3:
697                # If less than 3 points, ignore.
698                continue
699
700            polygon = Polygon.from_np_array(np_points)
701
702            # Split further based on shapley library,
703            # since some contours generated by opencv is consider invalid in shapely.
704            shapely_polygon = polygon.to_shapely_polygon()
705            shapely_valid_geom = shapely_make_valid(shapely_polygon)
706
707            if isinstance(shapely_valid_geom, ShapelyPolygon):
708                polygons.append(polygon)
709
710            elif isinstance(shapely_valid_geom, (ShapelyMultiPolygon, ShapelyGeometryCollection)):
711                for shapely_geom in shapely_valid_geom.geoms:
712                    if isinstance(shapely_geom, ShapelyPolygon):
713                        polygons.append(Polygon.from_shapely_polygon(shapely_geom))
714                    elif isinstance(shapely_geom, ShapelyMultiPolygon):
715                        # I don't know why, but this do happen.
716                        for shapely_sub_geom in shapely_geom.geoms:
717                            if isinstance(shapely_sub_geom, ShapelyPolygon):
718                                polygons.append(Polygon.from_shapely_polygon(shapely_sub_geom))
719                            else:
720                                logger.debug(f'ignore shapely_sub_geom={shapely_sub_geom}')
721                    else:
722                        logger.debug(f'ignore shapely_geom={shapely_geom}')
723
724            else:
725                logger.debug(f'ignore shapely_valid_geom={shapely_valid_geom}')
726
727        # Reset logging level.
728        shapely_geos_logger.setLevel(shapely_geos_logger_level)
729
730        return polygons
731
732    def to_disconnected_polygon_mask_pairs(
733        self,
734        cv_find_contours_method: int = cv.CHAIN_APPROX_SIMPLE,
735    ) -> Sequence[Tuple['Polygon', 'Mask']]:
736        pairs: List[Tuple[Polygon, Mask]] = []
737
738        for polygon in self.to_disconnected_polygons(
739            cv_find_contours_method=cv_find_contours_method,
740        ):
741            bounding_box = polygon.to_bounding_box()
742            boxed_mask = Mask.from_shapable(bounding_box).to_box_attached(bounding_box)
743            polygon.fill_mask(boxed_mask)
744            pairs.append((polygon, boxed_mask))
745
746        return pairs
747
748
749def generate_fill_by_masks_mask(
750    shape: Tuple[int, int],
751    masks: Iterable[Mask],
752    mode: ElementSetOperationMode,
753):
754    if mode == ElementSetOperationMode.UNION:
755        return None
756    else:
757        return Mask.from_masks(shape, masks, mode)
758
759
760# Cyclic dependency, by design.
761from .uniqueness import check_elements_uniqueness  # noqa: E402
762from .image import Image  # noqa: E402
763from .box import Box, generate_fill_by_boxes_mask  # noqa: E402
764from .polygon import Polygon, generate_fill_by_polygons_mask  # noqa: E402
765from .score_map import ScoreMap  # noqa: E402
class MaskSetItemConfig:
37class MaskSetItemConfig:
38    value: Union['Mask', np.ndarray, int] = 1
39    keep_max_value: bool = False
40    keep_min_value: bool = False
MaskSetItemConfig( value: Union[vkit.element.mask.Mask, numpy.ndarray, int] = 1, keep_max_value: bool = False, keep_min_value: bool = False)
2def __init__(self, value=attr_dict['value'].default, keep_max_value=attr_dict['keep_max_value'].default, keep_min_value=attr_dict['keep_min_value'].default):
3    self.value = value
4    self.keep_max_value = keep_max_value
5    self.keep_min_value = keep_min_value

Method generated by attrs for class MaskSetItemConfig.

class WritableMaskContextDecorator(contextlib.ContextDecorator):
43class WritableMaskContextDecorator(ContextDecorator):
44
45    def __init__(self, mask: 'Mask'):
46        super().__init__()
47        self.mask = mask
48
49    def __enter__(self):
50        if self.mask.mat.flags.c_contiguous:
51            assert not self.mask.mat.flags.writeable
52
53        try:
54            self.mask.mat.flags.writeable = True
55        except ValueError:
56            # Copy on write.
57            object.__setattr__(
58                self.mask,
59                'mat',
60                np.array(self.mask.mat),
61            )
62            assert self.mask.mat.flags.writeable
63
64    def __exit__(self, *exc):  # type: ignore
65        self.mask.mat.flags.writeable = False
66        self.mask.set_np_mask_out_of_date()

A base class or mixin that enables context managers to work as decorators.

WritableMaskContextDecorator(mask: vkit.element.mask.Mask)
45    def __init__(self, mask: 'Mask'):
46        super().__init__()
47        self.mask = mask
class Mask(vkit.element.type.Shapable):
 73class Mask(Shapable):
 74    mat: np.ndarray
 75    box: Optional['Box'] = None
 76
 77    _np_mask: Optional[np.ndarray] = attrs_lazy_field()
 78
 79    def __attrs_post_init__(self):
 80        if self.mat.dtype != np.uint8:
 81            raise RuntimeError('mat.dtype != np.uint8')
 82        if self.mat.ndim != 2:
 83            raise RuntimeError('ndim should == 2.')
 84
 85        # For the control of write.
 86        self.mat.flags.writeable = False
 87
 88        if self.box and self.shape != self.box.shape:
 89            raise RuntimeError('self.shape != box.shape.')
 90
 91    def lazy_post_init_np_mask(self):
 92        if self._np_mask is not None:
 93            return self._np_mask
 94
 95        object.__setattr__(self, '_np_mask', (self.mat > 0))
 96        return cast(np.ndarray, self._np_mask)
 97
 98    ###############
 99    # Constructor #
100    ###############
101    @classmethod
102    def from_shape(cls, shape: Tuple[int, int], value: int = 0):
103        height, width = shape
104        if value == 0:
105            np_init_func = np.zeros
106        else:
107            assert value == 1
108            np_init_func = np.ones
109        mat = np_init_func((height, width), dtype=np.uint8)
110        return cls(mat=mat)
111
112    @classmethod
113    def from_shapable(cls, shapable: Shapable, value: int = 0):
114        return cls.from_shape(shape=shapable.shape, value=value)
115
116    @classmethod
117    def _unpack_shape_or_box(cls, shape_or_box: Union[Tuple[int, int], 'Box']):
118        if isinstance(shape_or_box, Box):
119            attached_box = shape_or_box
120            shape = attached_box.shape
121        else:
122            attached_box = None
123            shape = shape_or_box
124        return shape, attached_box
125
126    @classmethod
127    def _from_np_active_count(
128        cls,
129        shape: Tuple[int, int],
130        mode: ElementSetOperationMode,
131        np_active_count: np.ndarray,
132        attached_box: Optional['Box'],
133    ):
134        mask = Mask.from_shape(shape)
135
136        with mask.writable_context:
137            if mode == ElementSetOperationMode.UNION:
138                mask.mat[np_active_count > 0] = 1
139
140            elif mode == ElementSetOperationMode.DISTINCT:
141                mask.mat[np_active_count == 1] = 1
142
143            elif mode == ElementSetOperationMode.INTERSECT:
144                mask.mat[np_active_count > 1] = 1
145
146            else:
147                raise NotImplementedError()
148
149        if attached_box:
150            mask = mask.to_box_attached(attached_box)
151
152        return mask
153
154    @classmethod
155    def from_boxes(
156        cls,
157        shape_or_box: Union[Tuple[int, int], 'Box'],
158        boxes: Iterable['Box'],
159        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
160    ):
161        shape, attached_box = cls._unpack_shape_or_box(shape_or_box)
162        np_active_count = np.zeros(shape, dtype=np.int32)
163
164        for box in boxes:
165            if attached_box:
166                box = box.to_relative_box(
167                    origin_y=attached_box.up,
168                    origin_x=attached_box.left,
169                )
170            np_boxed_active_count = box.extract_np_array(np_active_count)
171            np_boxed_active_count += 1
172
173        return cls._from_np_active_count(shape, mode, np_active_count, attached_box)
174
175    @classmethod
176    def from_polygons(
177        cls,
178        shape_or_box: Union[Tuple[int, int], 'Box'],
179        polygons: Iterable['Polygon'],
180        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
181    ):
182        shape, attached_box = cls._unpack_shape_or_box(shape_or_box)
183        np_active_count = np.zeros(shape, dtype=np.int32)
184
185        for polygon in polygons:
186            box = polygon.bounding_box
187            if attached_box:
188                box = box.to_relative_box(
189                    origin_y=attached_box.up,
190                    origin_x=attached_box.left,
191                )
192            np_boxed_active_count = box.extract_np_array(np_active_count)
193            np_boxed_active_count[polygon.internals.np_mask] += 1
194
195        return cls._from_np_active_count(shape, mode, np_active_count, attached_box)
196
197    @classmethod
198    def from_masks(
199        cls,
200        shape_or_box: Union[Tuple[int, int], 'Box'],
201        masks: Iterable['Mask'],
202        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
203    ):
204        shape, attached_box = cls._unpack_shape_or_box(shape_or_box)
205        np_active_count = np.zeros(shape, dtype=np.int32)
206
207        for mask in masks:
208            if mask.box:
209                box = mask.box
210                if attached_box:
211                    box = box.to_relative_box(
212                        origin_y=attached_box.up,
213                        origin_x=attached_box.left,
214                    )
215                np_boxed_active_count = box.extract_np_array(np_active_count)
216            else:
217                np_boxed_active_count = np_active_count
218            np_boxed_active_count[mask.np_mask] += 1
219
220        return cls._from_np_active_count(shape, mode, np_active_count, attached_box)
221
222    @classmethod
223    def from_score_maps(
224        cls,
225        shape_or_box: Union[Tuple[int, int], 'Box'],
226        score_maps: Iterable['ScoreMap'],
227        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
228    ):
229        shape, attached_box = cls._unpack_shape_or_box(shape_or_box)
230        np_active_count = np.zeros(shape, dtype=np.int32)
231
232        for score_map in score_maps:
233            if score_map.box:
234                box = score_map.box
235                if attached_box:
236                    box = box.to_relative_box(
237                        origin_y=attached_box.up,
238                        origin_x=attached_box.left,
239                    )
240                np_boxed_active_count = box.extract_np_array(np_active_count)
241            else:
242                np_boxed_active_count = np_active_count
243            np_boxed_active_count[score_map.to_mask().np_mask] += 1
244
245        return cls._from_np_active_count(shape, mode, np_active_count, attached_box)
246
247    ############
248    # Property #
249    ############
250    @property
251    def height(self):
252        return self.mat.shape[0]
253
254    @property
255    def width(self):
256        return self.mat.shape[1]
257
258    @property
259    def equivalent_box(self):
260        return self.box or Box.from_shapable(self)
261
262    @property
263    def np_mask(self):
264        return self.lazy_post_init_np_mask()
265
266    @property
267    def writable_context(self):
268        return WritableMaskContextDecorator(self)
269
270    ############
271    # Operator #
272    ############
273    def copy(self):
274        return attrs.evolve(self, mat=self.mat.copy())
275
276    def set_np_mask_out_of_date(self):
277        object.__setattr__(self, '_np_mask', None)
278
279    def assign_mat(self, mat: np.ndarray):
280        with self.writable_context:
281            object.__setattr__(self, 'mat', mat)
282
283    @classmethod
284    def unpack_element_value_pairs(
285        cls,
286        element_value_pairs: Iterable[Tuple[_E, Union['Mask', np.ndarray, int]]],
287    ):
288        elements: List[_E] = []
289        values: List[Union[Mask, np.ndarray, int]] = []
290        for element, value in element_value_pairs:
291            elements.append(element)
292            values.append(value)
293        return elements, values
294
295    def fill_by_box_value_pairs(
296        self,
297        box_value_pairs: Iterable[Tuple['Box', Union['Mask', np.ndarray, int]]],
298        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
299        keep_max_value: bool = False,
300        keep_min_value: bool = False,
301        skip_values_uniqueness_check: bool = False,
302    ):
303        boxes, values = self.unpack_element_value_pairs(box_value_pairs)
304
305        boxes_mask = generate_fill_by_boxes_mask(self.shape, boxes, mode)
306        if boxes_mask is None:
307            for box, value in zip(boxes, values):
308                box.fill_mask(
309                    mask=self,
310                    value=value,
311                    keep_max_value=keep_max_value,
312                    keep_min_value=keep_min_value,
313                )
314
315        else:
316            unique = True
317            if not skip_values_uniqueness_check:
318                unique = check_elements_uniqueness(values)
319
320            if unique:
321                boxes_mask.fill_mask(
322                    mask=self,
323                    value=values[0],
324                    keep_max_value=keep_max_value,
325                    keep_min_value=keep_min_value,
326                )
327            else:
328                for box, value in zip(boxes, values):
329                    box_mask = box.extract_mask(boxes_mask).to_box_attached(box)
330                    box_mask.fill_mask(
331                        mask=self,
332                        value=value,
333                        keep_max_value=keep_max_value,
334                        keep_min_value=keep_min_value,
335                    )
336
337    def fill_by_boxes(
338        self,
339        boxes: Iterable['Box'],
340        value: Union['Mask', np.ndarray, int] = 1,
341        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
342        keep_max_value: bool = False,
343        keep_min_value: bool = False,
344    ):
345        self.fill_by_box_value_pairs(
346            box_value_pairs=((box, value) for box in boxes),
347            mode=mode,
348            keep_max_value=keep_max_value,
349            keep_min_value=keep_min_value,
350            skip_values_uniqueness_check=True,
351        )
352
353    def fill_by_polygon_value_pairs(
354        self,
355        polygon_value_pairs: Iterable[Tuple['Polygon', Union['Mask', np.ndarray, int]]],
356        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
357        keep_max_value: bool = False,
358        keep_min_value: bool = False,
359        skip_values_uniqueness_check: bool = False,
360    ):
361        polygons, values = self.unpack_element_value_pairs(polygon_value_pairs)
362
363        polygons_mask = generate_fill_by_polygons_mask(self.shape, polygons, mode)
364        if polygons_mask is None:
365            for polygon, value in zip(polygons, values):
366                polygon.fill_mask(
367                    mask=self,
368                    value=value,
369                    keep_max_value=keep_max_value,
370                    keep_min_value=keep_min_value,
371                )
372
373        else:
374            unique = True
375            if not skip_values_uniqueness_check:
376                unique = check_elements_uniqueness(values)
377
378            if unique:
379                polygons_mask.fill_mask(
380                    mask=self,
381                    value=values[0],
382                    keep_max_value=keep_max_value,
383                    keep_min_value=keep_min_value,
384                )
385            else:
386                for polygon, value in zip(polygons, values):
387                    bounding_box = polygon.to_bounding_box()
388                    polygon_mask = bounding_box.extract_mask(polygons_mask)
389                    polygon_mask = polygon_mask.to_box_attached(bounding_box)
390                    polygon_mask.fill_mask(
391                        mask=self,
392                        value=value,
393                        keep_max_value=keep_max_value,
394                        keep_min_value=keep_min_value,
395                    )
396
397    def fill_by_polygons(
398        self,
399        polygons: Iterable['Polygon'],
400        value: Union['Mask', np.ndarray, int] = 1,
401        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
402        keep_max_value: bool = False,
403        keep_min_value: bool = False,
404    ):
405        self.fill_by_polygon_value_pairs(
406            polygon_value_pairs=((polygon, value) for polygon in polygons),
407            mode=mode,
408            keep_max_value=keep_max_value,
409            keep_min_value=keep_min_value,
410            skip_values_uniqueness_check=True,
411        )
412
413    def __setitem__(
414        self,
415        element: Union['Box', 'Polygon'],
416        config: Union[
417            'Mask',
418            np.ndarray,
419            int,
420            MaskSetItemConfig,
421        ],
422    ):  # yapf: disable
423        if not isinstance(config, MaskSetItemConfig):
424            value = config
425            keep_max_value = False
426            keep_min_value = False
427        else:
428            assert isinstance(config, MaskSetItemConfig)
429            value = config.value
430            keep_max_value = config.keep_max_value
431            keep_min_value = config.keep_min_value
432
433        element.fill_mask(
434            mask=self,
435            value=value,
436            keep_min_value=keep_min_value,
437            keep_max_value=keep_max_value,
438        )
439
440    def __getitem__(
441        self,
442        element: Union['Box', 'Polygon'],
443    ):
444        return element.extract_mask(self)
445
446    def to_inverted_mask(self):
447        mat = (~self.np_mask).astype(np.uint8)
448        return attrs.evolve(self, mat=mat)
449
450    def to_shifted_mask(self, offset_y: int = 0, offset_x: int = 0):
451        assert self.box
452        shifted_box = self.box.to_shifted_box(offset_y=offset_y, offset_x=offset_x)
453        return attrs.evolve(self, box=shifted_box)
454
455    def to_resized_mask(
456        self,
457        resized_height: Optional[int] = None,
458        resized_width: Optional[int] = None,
459        cv_resize_interpolation: int = cv.INTER_CUBIC,
460        binarization_threshold: int = 0,
461    ):
462        assert not self.box
463        resized_height, resized_width = generate_resized_shape(
464            height=self.height,
465            width=self.width,
466            resized_height=resized_height,
467            resized_width=resized_width,
468        )
469
470        # Deal with precision loss.
471        mat = self.np_mask.astype(np.uint8) * 255
472        mat = cv.resize(
473            mat,
474            (resized_width, resized_height),
475            interpolation=cv_resize_interpolation,
476        )
477        mat = cast(np.ndarray, mat)
478        mat = (mat > binarization_threshold).astype(np.uint8)
479
480        return Mask(mat=mat)
481
482    def to_conducted_resized_mask(
483        self,
484        shapable_or_shape: Union[Shapable, Tuple[int, int]],
485        resized_height: Optional[int] = None,
486        resized_width: Optional[int] = None,
487        cv_resize_interpolation: int = cv.INTER_CUBIC,
488        binarization_threshold: int = 0,
489    ):
490        assert self.box
491        resized_box = self.box.to_conducted_resized_box(
492            shapable_or_shape=shapable_or_shape,
493            resized_height=resized_height,
494            resized_width=resized_width,
495        )
496        resized_mask = self.to_box_detached().to_resized_mask(
497            resized_height=resized_box.height,
498            resized_width=resized_box.width,
499            cv_resize_interpolation=cv_resize_interpolation,
500            binarization_threshold=binarization_threshold,
501        )
502        resized_mask = resized_mask.to_box_attached(resized_box)
503        return resized_mask
504
505    def to_cropped_mask(
506        self,
507        up: Optional[int] = None,
508        down: Optional[int] = None,
509        left: Optional[int] = None,
510        right: Optional[int] = None,
511    ):
512        assert not self.box
513
514        up = up or 0
515        down = down or self.height - 1
516        left = left or 0
517        right = right or self.width - 1
518
519        return attrs.evolve(self, mat=self.mat[up:down + 1, left:right + 1])
520
521    def to_box_attached(self, box: 'Box'):
522        assert self.height == box.height
523        assert self.width == box.width
524        return attrs.evolve(self, box=box)
525
526    def to_box_detached(self):
527        assert self.box
528        return attrs.evolve(self, box=None)
529
530    def fill_np_array(
531        self,
532        mat: np.ndarray,
533        value: Union[np.ndarray, Tuple[float, ...], float],
534        alpha: Union['ScoreMap', np.ndarray, float] = 1.0,
535        keep_max_value: bool = False,
536        keep_min_value: bool = False,
537    ):
538        self.equivalent_box.fill_np_array(
539            mat=mat,
540            value=value,
541            np_mask=self.np_mask,
542            alpha=alpha,
543            keep_max_value=keep_max_value,
544            keep_min_value=keep_min_value,
545        )
546
547    def extract_mask(self, mask: 'Mask'):
548        mask = self.equivalent_box.extract_mask(mask)
549
550        mask = mask.copy()
551        self.to_inverted_mask().fill_mask(mask, value=0)
552        return mask
553
554    def fill_mask(
555        self,
556        mask: 'Mask',
557        value: Union['Mask', np.ndarray, int] = 1,
558        keep_max_value: bool = False,
559        keep_min_value: bool = False,
560    ):
561        self.equivalent_box.fill_mask(
562            mask=mask,
563            value=value,
564            mask_mask=self,
565            keep_max_value=keep_max_value,
566            keep_min_value=keep_min_value,
567        )
568
569    def extract_score_map(self, score_map: 'ScoreMap'):
570        score_map = self.equivalent_box.extract_score_map(score_map)
571
572        score_map = score_map.copy()
573        self.to_inverted_mask().fill_score_map(score_map, value=0.0)
574        return score_map
575
576    def fill_score_map(
577        self,
578        score_map: 'ScoreMap',
579        value: Union['ScoreMap', np.ndarray, float],
580        keep_max_value: bool = False,
581        keep_min_value: bool = False,
582    ):
583        self.equivalent_box.fill_score_map(
584            score_map=score_map,
585            value=value,
586            score_map_mask=self,
587            keep_max_value=keep_max_value,
588            keep_min_value=keep_min_value,
589        )
590
591    def to_score_map(self):
592        mat = self.np_mask.astype(np.float32)
593        return ScoreMap(mat=mat, box=self.box)
594
595    def extract_image(self, image: 'Image'):
596        image = self.equivalent_box.extract_image(image)
597
598        image = image.copy()
599        self.to_inverted_mask().fill_image(image, value=0)
600        return image
601
602    def fill_image(
603        self,
604        image: 'Image',
605        value: Union['Image', np.ndarray, Tuple[int, ...], int],
606        alpha: Union['ScoreMap', np.ndarray, float] = 1.0,
607    ):
608        self.equivalent_box.fill_image(
609            image=image,
610            value=value,
611            image_mask=self,
612            alpha=alpha,
613        )
614
615    def to_external_box(self):
616        np_mask = self.np_mask
617
618        np_vert_max: np.ndarray = np.amax(np_mask, axis=1)
619        np_vert_nonzero = np.nonzero(np_vert_max)[0]
620        if len(np_vert_nonzero) == 0:
621            raise RuntimeError('to_external_box: empty np_mask.')
622
623        up = int(np_vert_nonzero[0])
624        down = int(np_vert_nonzero[-1])
625
626        np_hori_max: np.ndarray = np.amax(np_mask, axis=0)
627        np_hori_nonzero = np.nonzero(np_hori_max)[0]
628        if len(np_hori_nonzero) == 0:
629            raise RuntimeError('to_external_box: empty np_mask.')
630
631        left = int(np_hori_nonzero[0])
632        right = int(np_hori_nonzero[-1])
633
634        return Box(up=up, down=down, left=left, right=right)
635
636    def to_external_polygon(
637        self,
638        cv_find_contours_method: int = cv.CHAIN_APPROX_SIMPLE,
639    ):
640        polygons = self.to_disconnected_polygons(cv_find_contours_method=cv_find_contours_method)
641        if not polygons:
642            raise RuntimeError('Cannot find any contour.')
643        elif len(polygons) > 1:
644            logger.warning(
645                'More than one polygons is detected, keep the largest one as the external polygon.'
646            )
647            area_max = 0
648            best_polygon = None
649            for polygon in polygons:
650                if polygon.area > area_max:
651                    area_max = polygon.area
652                    best_polygon = polygon
653            assert best_polygon
654            return best_polygon
655        else:
656            return polygons[0]
657
658    def to_disconnected_polygons(
659        self,
660        cv_find_contours_method: int = cv.CHAIN_APPROX_SIMPLE,
661    ) -> Sequence['Polygon']:
662        # [ (N, 1, 2), ... ]
663        # https://stackoverflow.com/a/8830981
664        # https://docs.opencv.org/4.x/d9/d8b/tutorial_py_contours_hierarchy.html
665        cv_contours, cv_hierarchy = cv.findContours(
666            (self.np_mask.astype(np.uint8) * 255),
667            cv.RETR_TREE,
668            cv_find_contours_method,
669        )
670        if not cv_contours:
671            return []
672
673        assert len(cv_hierarchy) == 1
674        assert len(cv_contours) == len(cv_hierarchy[0])
675        cv_hierarchy = cv_hierarchy[0]
676
677        polygons: List[Polygon] = []
678
679        # Ignore logging of shapely.geos.
680        shapely_geos_logger = logging.getLogger('shapely.geos')
681        shapely_geos_logger_level = shapely_geos_logger.level
682        shapely_geos_logger.setLevel(logging.WARNING)
683
684        for cv_contour, cv_contour_hierarchy in zip(cv_contours, cv_hierarchy):
685            assert len(cv_contour_hierarchy) == 4
686            cv_contour_parent = cv_contour_hierarchy[-1]
687            if cv_contour_parent >= 0:
688                continue
689
690            assert cv_contour.shape[1] == 1
691            np_points = np.squeeze(cv_contour, axis=1)
692
693            if self.box:
694                np_points[:, 0] += self.box.left
695                np_points[:, 1] += self.box.up
696
697            if np_points.shape[0] < 3:
698                # If less than 3 points, ignore.
699                continue
700
701            polygon = Polygon.from_np_array(np_points)
702
703            # Split further based on shapley library,
704            # since some contours generated by opencv is consider invalid in shapely.
705            shapely_polygon = polygon.to_shapely_polygon()
706            shapely_valid_geom = shapely_make_valid(shapely_polygon)
707
708            if isinstance(shapely_valid_geom, ShapelyPolygon):
709                polygons.append(polygon)
710
711            elif isinstance(shapely_valid_geom, (ShapelyMultiPolygon, ShapelyGeometryCollection)):
712                for shapely_geom in shapely_valid_geom.geoms:
713                    if isinstance(shapely_geom, ShapelyPolygon):
714                        polygons.append(Polygon.from_shapely_polygon(shapely_geom))
715                    elif isinstance(shapely_geom, ShapelyMultiPolygon):
716                        # I don't know why, but this do happen.
717                        for shapely_sub_geom in shapely_geom.geoms:
718                            if isinstance(shapely_sub_geom, ShapelyPolygon):
719                                polygons.append(Polygon.from_shapely_polygon(shapely_sub_geom))
720                            else:
721                                logger.debug(f'ignore shapely_sub_geom={shapely_sub_geom}')
722                    else:
723                        logger.debug(f'ignore shapely_geom={shapely_geom}')
724
725            else:
726                logger.debug(f'ignore shapely_valid_geom={shapely_valid_geom}')
727
728        # Reset logging level.
729        shapely_geos_logger.setLevel(shapely_geos_logger_level)
730
731        return polygons
732
733    def to_disconnected_polygon_mask_pairs(
734        self,
735        cv_find_contours_method: int = cv.CHAIN_APPROX_SIMPLE,
736    ) -> Sequence[Tuple['Polygon', 'Mask']]:
737        pairs: List[Tuple[Polygon, Mask]] = []
738
739        for polygon in self.to_disconnected_polygons(
740            cv_find_contours_method=cv_find_contours_method,
741        ):
742            bounding_box = polygon.to_bounding_box()
743            boxed_mask = Mask.from_shapable(bounding_box).to_box_attached(bounding_box)
744            polygon.fill_mask(boxed_mask)
745            pairs.append((polygon, boxed_mask))
746
747        return pairs
Mask( mat: numpy.ndarray, box: Union[vkit.element.box.Box, NoneType] = None)
2def __init__(self, mat, box=attr_dict['box'].default):
3    _setattr(self, 'mat', mat)
4    _setattr(self, 'box', box)
5    _setattr(self, '_np_mask', attr_dict['_np_mask'].default)
6    self.__attrs_post_init__()

Method generated by attrs for class Mask.

def lazy_post_init_np_mask(self):
91    def lazy_post_init_np_mask(self):
92        if self._np_mask is not None:
93            return self._np_mask
94
95        object.__setattr__(self, '_np_mask', (self.mat > 0))
96        return cast(np.ndarray, self._np_mask)
@classmethod
def from_shape(cls, shape: Tuple[int, int], value: int = 0):
101    @classmethod
102    def from_shape(cls, shape: Tuple[int, int], value: int = 0):
103        height, width = shape
104        if value == 0:
105            np_init_func = np.zeros
106        else:
107            assert value == 1
108            np_init_func = np.ones
109        mat = np_init_func((height, width), dtype=np.uint8)
110        return cls(mat=mat)
@classmethod
def from_shapable(cls, shapable: vkit.element.type.Shapable, value: int = 0):
112    @classmethod
113    def from_shapable(cls, shapable: Shapable, value: int = 0):
114        return cls.from_shape(shape=shapable.shape, value=value)
@classmethod
def from_boxes( cls, shape_or_box: Union[Tuple[int, int], vkit.element.box.Box], boxes: collections.abc.Iterable[vkit.element.box.Box], mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>):
154    @classmethod
155    def from_boxes(
156        cls,
157        shape_or_box: Union[Tuple[int, int], 'Box'],
158        boxes: Iterable['Box'],
159        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
160    ):
161        shape, attached_box = cls._unpack_shape_or_box(shape_or_box)
162        np_active_count = np.zeros(shape, dtype=np.int32)
163
164        for box in boxes:
165            if attached_box:
166                box = box.to_relative_box(
167                    origin_y=attached_box.up,
168                    origin_x=attached_box.left,
169                )
170            np_boxed_active_count = box.extract_np_array(np_active_count)
171            np_boxed_active_count += 1
172
173        return cls._from_np_active_count(shape, mode, np_active_count, attached_box)
@classmethod
def from_polygons( cls, shape_or_box: Union[Tuple[int, int], vkit.element.box.Box], polygons: collections.abc.Iterable[vkit.element.polygon.Polygon], mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>):
175    @classmethod
176    def from_polygons(
177        cls,
178        shape_or_box: Union[Tuple[int, int], 'Box'],
179        polygons: Iterable['Polygon'],
180        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
181    ):
182        shape, attached_box = cls._unpack_shape_or_box(shape_or_box)
183        np_active_count = np.zeros(shape, dtype=np.int32)
184
185        for polygon in polygons:
186            box = polygon.bounding_box
187            if attached_box:
188                box = box.to_relative_box(
189                    origin_y=attached_box.up,
190                    origin_x=attached_box.left,
191                )
192            np_boxed_active_count = box.extract_np_array(np_active_count)
193            np_boxed_active_count[polygon.internals.np_mask] += 1
194
195        return cls._from_np_active_count(shape, mode, np_active_count, attached_box)
@classmethod
def from_masks( cls, shape_or_box: Union[Tuple[int, int], vkit.element.box.Box], masks: collections.abc.Iterable[vkit.element.mask.Mask], mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>):
197    @classmethod
198    def from_masks(
199        cls,
200        shape_or_box: Union[Tuple[int, int], 'Box'],
201        masks: Iterable['Mask'],
202        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
203    ):
204        shape, attached_box = cls._unpack_shape_or_box(shape_or_box)
205        np_active_count = np.zeros(shape, dtype=np.int32)
206
207        for mask in masks:
208            if mask.box:
209                box = mask.box
210                if attached_box:
211                    box = box.to_relative_box(
212                        origin_y=attached_box.up,
213                        origin_x=attached_box.left,
214                    )
215                np_boxed_active_count = box.extract_np_array(np_active_count)
216            else:
217                np_boxed_active_count = np_active_count
218            np_boxed_active_count[mask.np_mask] += 1
219
220        return cls._from_np_active_count(shape, mode, np_active_count, attached_box)
@classmethod
def from_score_maps( cls, shape_or_box: Union[Tuple[int, int], vkit.element.box.Box], score_maps: collections.abc.Iterable[vkit.element.score_map.ScoreMap], mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>):
222    @classmethod
223    def from_score_maps(
224        cls,
225        shape_or_box: Union[Tuple[int, int], 'Box'],
226        score_maps: Iterable['ScoreMap'],
227        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
228    ):
229        shape, attached_box = cls._unpack_shape_or_box(shape_or_box)
230        np_active_count = np.zeros(shape, dtype=np.int32)
231
232        for score_map in score_maps:
233            if score_map.box:
234                box = score_map.box
235                if attached_box:
236                    box = box.to_relative_box(
237                        origin_y=attached_box.up,
238                        origin_x=attached_box.left,
239                    )
240                np_boxed_active_count = box.extract_np_array(np_active_count)
241            else:
242                np_boxed_active_count = np_active_count
243            np_boxed_active_count[score_map.to_mask().np_mask] += 1
244
245        return cls._from_np_active_count(shape, mode, np_active_count, attached_box)
def copy(self):
273    def copy(self):
274        return attrs.evolve(self, mat=self.mat.copy())
def set_np_mask_out_of_date(self):
276    def set_np_mask_out_of_date(self):
277        object.__setattr__(self, '_np_mask', None)
def assign_mat(self, mat: numpy.ndarray):
279    def assign_mat(self, mat: np.ndarray):
280        with self.writable_context:
281            object.__setattr__(self, 'mat', mat)
@classmethod
def unpack_element_value_pairs( cls, element_value_pairs: collections.abc.Iterable[tuple[~_E, typing.Union[vkit.element.mask.Mask, numpy.ndarray, int]]]):
283    @classmethod
284    def unpack_element_value_pairs(
285        cls,
286        element_value_pairs: Iterable[Tuple[_E, Union['Mask', np.ndarray, int]]],
287    ):
288        elements: List[_E] = []
289        values: List[Union[Mask, np.ndarray, int]] = []
290        for element, value in element_value_pairs:
291            elements.append(element)
292            values.append(value)
293        return elements, values
def fill_by_box_value_pairs( self, box_value_pairs: collections.abc.Iterable[tuple[vkit.element.box.Box, typing.Union[vkit.element.mask.Mask, numpy.ndarray, int]]], mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>, keep_max_value: bool = False, keep_min_value: bool = False, skip_values_uniqueness_check: bool = False):
295    def fill_by_box_value_pairs(
296        self,
297        box_value_pairs: Iterable[Tuple['Box', Union['Mask', np.ndarray, int]]],
298        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
299        keep_max_value: bool = False,
300        keep_min_value: bool = False,
301        skip_values_uniqueness_check: bool = False,
302    ):
303        boxes, values = self.unpack_element_value_pairs(box_value_pairs)
304
305        boxes_mask = generate_fill_by_boxes_mask(self.shape, boxes, mode)
306        if boxes_mask is None:
307            for box, value in zip(boxes, values):
308                box.fill_mask(
309                    mask=self,
310                    value=value,
311                    keep_max_value=keep_max_value,
312                    keep_min_value=keep_min_value,
313                )
314
315        else:
316            unique = True
317            if not skip_values_uniqueness_check:
318                unique = check_elements_uniqueness(values)
319
320            if unique:
321                boxes_mask.fill_mask(
322                    mask=self,
323                    value=values[0],
324                    keep_max_value=keep_max_value,
325                    keep_min_value=keep_min_value,
326                )
327            else:
328                for box, value in zip(boxes, values):
329                    box_mask = box.extract_mask(boxes_mask).to_box_attached(box)
330                    box_mask.fill_mask(
331                        mask=self,
332                        value=value,
333                        keep_max_value=keep_max_value,
334                        keep_min_value=keep_min_value,
335                    )
def fill_by_boxes( self, boxes: collections.abc.Iterable[vkit.element.box.Box], value: Union[vkit.element.mask.Mask, numpy.ndarray, int] = 1, mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>, keep_max_value: bool = False, keep_min_value: bool = False):
337    def fill_by_boxes(
338        self,
339        boxes: Iterable['Box'],
340        value: Union['Mask', np.ndarray, int] = 1,
341        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
342        keep_max_value: bool = False,
343        keep_min_value: bool = False,
344    ):
345        self.fill_by_box_value_pairs(
346            box_value_pairs=((box, value) for box in boxes),
347            mode=mode,
348            keep_max_value=keep_max_value,
349            keep_min_value=keep_min_value,
350            skip_values_uniqueness_check=True,
351        )
def fill_by_polygon_value_pairs( self, polygon_value_pairs: collections.abc.Iterable[tuple[vkit.element.polygon.Polygon, typing.Union[vkit.element.mask.Mask, numpy.ndarray, int]]], mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>, keep_max_value: bool = False, keep_min_value: bool = False, skip_values_uniqueness_check: bool = False):
353    def fill_by_polygon_value_pairs(
354        self,
355        polygon_value_pairs: Iterable[Tuple['Polygon', Union['Mask', np.ndarray, int]]],
356        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
357        keep_max_value: bool = False,
358        keep_min_value: bool = False,
359        skip_values_uniqueness_check: bool = False,
360    ):
361        polygons, values = self.unpack_element_value_pairs(polygon_value_pairs)
362
363        polygons_mask = generate_fill_by_polygons_mask(self.shape, polygons, mode)
364        if polygons_mask is None:
365            for polygon, value in zip(polygons, values):
366                polygon.fill_mask(
367                    mask=self,
368                    value=value,
369                    keep_max_value=keep_max_value,
370                    keep_min_value=keep_min_value,
371                )
372
373        else:
374            unique = True
375            if not skip_values_uniqueness_check:
376                unique = check_elements_uniqueness(values)
377
378            if unique:
379                polygons_mask.fill_mask(
380                    mask=self,
381                    value=values[0],
382                    keep_max_value=keep_max_value,
383                    keep_min_value=keep_min_value,
384                )
385            else:
386                for polygon, value in zip(polygons, values):
387                    bounding_box = polygon.to_bounding_box()
388                    polygon_mask = bounding_box.extract_mask(polygons_mask)
389                    polygon_mask = polygon_mask.to_box_attached(bounding_box)
390                    polygon_mask.fill_mask(
391                        mask=self,
392                        value=value,
393                        keep_max_value=keep_max_value,
394                        keep_min_value=keep_min_value,
395                    )
def fill_by_polygons( self, polygons: collections.abc.Iterable[vkit.element.polygon.Polygon], value: Union[vkit.element.mask.Mask, numpy.ndarray, int] = 1, mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>, keep_max_value: bool = False, keep_min_value: bool = False):
397    def fill_by_polygons(
398        self,
399        polygons: Iterable['Polygon'],
400        value: Union['Mask', np.ndarray, int] = 1,
401        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
402        keep_max_value: bool = False,
403        keep_min_value: bool = False,
404    ):
405        self.fill_by_polygon_value_pairs(
406            polygon_value_pairs=((polygon, value) for polygon in polygons),
407            mode=mode,
408            keep_max_value=keep_max_value,
409            keep_min_value=keep_min_value,
410            skip_values_uniqueness_check=True,
411        )
def to_inverted_mask(self):
446    def to_inverted_mask(self):
447        mat = (~self.np_mask).astype(np.uint8)
448        return attrs.evolve(self, mat=mat)
def to_shifted_mask(self, offset_y: int = 0, offset_x: int = 0):
450    def to_shifted_mask(self, offset_y: int = 0, offset_x: int = 0):
451        assert self.box
452        shifted_box = self.box.to_shifted_box(offset_y=offset_y, offset_x=offset_x)
453        return attrs.evolve(self, box=shifted_box)
def to_resized_mask( self, resized_height: Union[int, NoneType] = None, resized_width: Union[int, NoneType] = None, cv_resize_interpolation: int = 2, binarization_threshold: int = 0):
455    def to_resized_mask(
456        self,
457        resized_height: Optional[int] = None,
458        resized_width: Optional[int] = None,
459        cv_resize_interpolation: int = cv.INTER_CUBIC,
460        binarization_threshold: int = 0,
461    ):
462        assert not self.box
463        resized_height, resized_width = generate_resized_shape(
464            height=self.height,
465            width=self.width,
466            resized_height=resized_height,
467            resized_width=resized_width,
468        )
469
470        # Deal with precision loss.
471        mat = self.np_mask.astype(np.uint8) * 255
472        mat = cv.resize(
473            mat,
474            (resized_width, resized_height),
475            interpolation=cv_resize_interpolation,
476        )
477        mat = cast(np.ndarray, mat)
478        mat = (mat > binarization_threshold).astype(np.uint8)
479
480        return Mask(mat=mat)
def to_conducted_resized_mask( 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, binarization_threshold: int = 0):
482    def to_conducted_resized_mask(
483        self,
484        shapable_or_shape: Union[Shapable, Tuple[int, int]],
485        resized_height: Optional[int] = None,
486        resized_width: Optional[int] = None,
487        cv_resize_interpolation: int = cv.INTER_CUBIC,
488        binarization_threshold: int = 0,
489    ):
490        assert self.box
491        resized_box = self.box.to_conducted_resized_box(
492            shapable_or_shape=shapable_or_shape,
493            resized_height=resized_height,
494            resized_width=resized_width,
495        )
496        resized_mask = self.to_box_detached().to_resized_mask(
497            resized_height=resized_box.height,
498            resized_width=resized_box.width,
499            cv_resize_interpolation=cv_resize_interpolation,
500            binarization_threshold=binarization_threshold,
501        )
502        resized_mask = resized_mask.to_box_attached(resized_box)
503        return resized_mask
def to_cropped_mask( self, up: Union[int, NoneType] = None, down: Union[int, NoneType] = None, left: Union[int, NoneType] = None, right: Union[int, NoneType] = None):
505    def to_cropped_mask(
506        self,
507        up: Optional[int] = None,
508        down: Optional[int] = None,
509        left: Optional[int] = None,
510        right: Optional[int] = None,
511    ):
512        assert not self.box
513
514        up = up or 0
515        down = down or self.height - 1
516        left = left or 0
517        right = right or self.width - 1
518
519        return attrs.evolve(self, mat=self.mat[up:down + 1, left:right + 1])
def to_box_attached(self, box: vkit.element.box.Box):
521    def to_box_attached(self, box: 'Box'):
522        assert self.height == box.height
523        assert self.width == box.width
524        return attrs.evolve(self, box=box)
def to_box_detached(self):
526    def to_box_detached(self):
527        assert self.box
528        return attrs.evolve(self, box=None)
def fill_np_array( self, mat: numpy.ndarray, value: Union[numpy.ndarray, Tuple[float, ...], float], alpha: Union[vkit.element.score_map.ScoreMap, numpy.ndarray, float] = 1.0, keep_max_value: bool = False, keep_min_value: bool = False):
530    def fill_np_array(
531        self,
532        mat: np.ndarray,
533        value: Union[np.ndarray, Tuple[float, ...], float],
534        alpha: Union['ScoreMap', np.ndarray, float] = 1.0,
535        keep_max_value: bool = False,
536        keep_min_value: bool = False,
537    ):
538        self.equivalent_box.fill_np_array(
539            mat=mat,
540            value=value,
541            np_mask=self.np_mask,
542            alpha=alpha,
543            keep_max_value=keep_max_value,
544            keep_min_value=keep_min_value,
545        )
def extract_mask(self, mask: vkit.element.mask.Mask):
547    def extract_mask(self, mask: 'Mask'):
548        mask = self.equivalent_box.extract_mask(mask)
549
550        mask = mask.copy()
551        self.to_inverted_mask().fill_mask(mask, value=0)
552        return mask
def fill_mask( self, mask: vkit.element.mask.Mask, value: Union[vkit.element.mask.Mask, numpy.ndarray, int] = 1, keep_max_value: bool = False, keep_min_value: bool = False):
554    def fill_mask(
555        self,
556        mask: 'Mask',
557        value: Union['Mask', np.ndarray, int] = 1,
558        keep_max_value: bool = False,
559        keep_min_value: bool = False,
560    ):
561        self.equivalent_box.fill_mask(
562            mask=mask,
563            value=value,
564            mask_mask=self,
565            keep_max_value=keep_max_value,
566            keep_min_value=keep_min_value,
567        )
def extract_score_map(self, score_map: vkit.element.score_map.ScoreMap):
569    def extract_score_map(self, score_map: 'ScoreMap'):
570        score_map = self.equivalent_box.extract_score_map(score_map)
571
572        score_map = score_map.copy()
573        self.to_inverted_mask().fill_score_map(score_map, value=0.0)
574        return score_map
def fill_score_map( self, score_map: vkit.element.score_map.ScoreMap, value: Union[vkit.element.score_map.ScoreMap, numpy.ndarray, float], keep_max_value: bool = False, keep_min_value: bool = False):
576    def fill_score_map(
577        self,
578        score_map: 'ScoreMap',
579        value: Union['ScoreMap', np.ndarray, float],
580        keep_max_value: bool = False,
581        keep_min_value: bool = False,
582    ):
583        self.equivalent_box.fill_score_map(
584            score_map=score_map,
585            value=value,
586            score_map_mask=self,
587            keep_max_value=keep_max_value,
588            keep_min_value=keep_min_value,
589        )
def to_score_map(self):
591    def to_score_map(self):
592        mat = self.np_mask.astype(np.float32)
593        return ScoreMap(mat=mat, box=self.box)
def extract_image(self, image: vkit.element.image.Image):
595    def extract_image(self, image: 'Image'):
596        image = self.equivalent_box.extract_image(image)
597
598        image = image.copy()
599        self.to_inverted_mask().fill_image(image, value=0)
600        return image
def fill_image( self, image: vkit.element.image.Image, value: Union[vkit.element.image.Image, numpy.ndarray, Tuple[int, ...], int], alpha: Union[vkit.element.score_map.ScoreMap, numpy.ndarray, float] = 1.0):
602    def fill_image(
603        self,
604        image: 'Image',
605        value: Union['Image', np.ndarray, Tuple[int, ...], int],
606        alpha: Union['ScoreMap', np.ndarray, float] = 1.0,
607    ):
608        self.equivalent_box.fill_image(
609            image=image,
610            value=value,
611            image_mask=self,
612            alpha=alpha,
613        )
def to_external_box(self):
615    def to_external_box(self):
616        np_mask = self.np_mask
617
618        np_vert_max: np.ndarray = np.amax(np_mask, axis=1)
619        np_vert_nonzero = np.nonzero(np_vert_max)[0]
620        if len(np_vert_nonzero) == 0:
621            raise RuntimeError('to_external_box: empty np_mask.')
622
623        up = int(np_vert_nonzero[0])
624        down = int(np_vert_nonzero[-1])
625
626        np_hori_max: np.ndarray = np.amax(np_mask, axis=0)
627        np_hori_nonzero = np.nonzero(np_hori_max)[0]
628        if len(np_hori_nonzero) == 0:
629            raise RuntimeError('to_external_box: empty np_mask.')
630
631        left = int(np_hori_nonzero[0])
632        right = int(np_hori_nonzero[-1])
633
634        return Box(up=up, down=down, left=left, right=right)
def to_external_polygon(self, cv_find_contours_method: int = 2):
636    def to_external_polygon(
637        self,
638        cv_find_contours_method: int = cv.CHAIN_APPROX_SIMPLE,
639    ):
640        polygons = self.to_disconnected_polygons(cv_find_contours_method=cv_find_contours_method)
641        if not polygons:
642            raise RuntimeError('Cannot find any contour.')
643        elif len(polygons) > 1:
644            logger.warning(
645                'More than one polygons is detected, keep the largest one as the external polygon.'
646            )
647            area_max = 0
648            best_polygon = None
649            for polygon in polygons:
650                if polygon.area > area_max:
651                    area_max = polygon.area
652                    best_polygon = polygon
653            assert best_polygon
654            return best_polygon
655        else:
656            return polygons[0]
def to_disconnected_polygons( self, cv_find_contours_method: int = 2) -> collections.abc.Sequence[vkit.element.polygon.Polygon]:
658    def to_disconnected_polygons(
659        self,
660        cv_find_contours_method: int = cv.CHAIN_APPROX_SIMPLE,
661    ) -> Sequence['Polygon']:
662        # [ (N, 1, 2), ... ]
663        # https://stackoverflow.com/a/8830981
664        # https://docs.opencv.org/4.x/d9/d8b/tutorial_py_contours_hierarchy.html
665        cv_contours, cv_hierarchy = cv.findContours(
666            (self.np_mask.astype(np.uint8) * 255),
667            cv.RETR_TREE,
668            cv_find_contours_method,
669        )
670        if not cv_contours:
671            return []
672
673        assert len(cv_hierarchy) == 1
674        assert len(cv_contours) == len(cv_hierarchy[0])
675        cv_hierarchy = cv_hierarchy[0]
676
677        polygons: List[Polygon] = []
678
679        # Ignore logging of shapely.geos.
680        shapely_geos_logger = logging.getLogger('shapely.geos')
681        shapely_geos_logger_level = shapely_geos_logger.level
682        shapely_geos_logger.setLevel(logging.WARNING)
683
684        for cv_contour, cv_contour_hierarchy in zip(cv_contours, cv_hierarchy):
685            assert len(cv_contour_hierarchy) == 4
686            cv_contour_parent = cv_contour_hierarchy[-1]
687            if cv_contour_parent >= 0:
688                continue
689
690            assert cv_contour.shape[1] == 1
691            np_points = np.squeeze(cv_contour, axis=1)
692
693            if self.box:
694                np_points[:, 0] += self.box.left
695                np_points[:, 1] += self.box.up
696
697            if np_points.shape[0] < 3:
698                # If less than 3 points, ignore.
699                continue
700
701            polygon = Polygon.from_np_array(np_points)
702
703            # Split further based on shapley library,
704            # since some contours generated by opencv is consider invalid in shapely.
705            shapely_polygon = polygon.to_shapely_polygon()
706            shapely_valid_geom = shapely_make_valid(shapely_polygon)
707
708            if isinstance(shapely_valid_geom, ShapelyPolygon):
709                polygons.append(polygon)
710
711            elif isinstance(shapely_valid_geom, (ShapelyMultiPolygon, ShapelyGeometryCollection)):
712                for shapely_geom in shapely_valid_geom.geoms:
713                    if isinstance(shapely_geom, ShapelyPolygon):
714                        polygons.append(Polygon.from_shapely_polygon(shapely_geom))
715                    elif isinstance(shapely_geom, ShapelyMultiPolygon):
716                        # I don't know why, but this do happen.
717                        for shapely_sub_geom in shapely_geom.geoms:
718                            if isinstance(shapely_sub_geom, ShapelyPolygon):
719                                polygons.append(Polygon.from_shapely_polygon(shapely_sub_geom))
720                            else:
721                                logger.debug(f'ignore shapely_sub_geom={shapely_sub_geom}')
722                    else:
723                        logger.debug(f'ignore shapely_geom={shapely_geom}')
724
725            else:
726                logger.debug(f'ignore shapely_valid_geom={shapely_valid_geom}')
727
728        # Reset logging level.
729        shapely_geos_logger.setLevel(shapely_geos_logger_level)
730
731        return polygons
def to_disconnected_polygon_mask_pairs( self, cv_find_contours_method: int = 2) -> collections.abc.Sequence[tuple[vkit.element.polygon.Polygon, vkit.element.mask.Mask]]:
733    def to_disconnected_polygon_mask_pairs(
734        self,
735        cv_find_contours_method: int = cv.CHAIN_APPROX_SIMPLE,
736    ) -> Sequence[Tuple['Polygon', 'Mask']]:
737        pairs: List[Tuple[Polygon, Mask]] = []
738
739        for polygon in self.to_disconnected_polygons(
740            cv_find_contours_method=cv_find_contours_method,
741        ):
742            bounding_box = polygon.to_bounding_box()
743            boxed_mask = Mask.from_shapable(bounding_box).to_box_attached(bounding_box)
744            polygon.fill_mask(boxed_mask)
745            pairs.append((polygon, boxed_mask))
746
747        return pairs
def generate_fill_by_masks_mask( shape: Tuple[int, int], masks: Iterable[vkit.element.mask.Mask], mode: vkit.element.type.ElementSetOperationMode):
750def generate_fill_by_masks_mask(
751    shape: Tuple[int, int],
752    masks: Iterable[Mask],
753    mode: ElementSetOperationMode,
754):
755    if mode == ElementSetOperationMode.UNION:
756        return None
757    else:
758        return Mask.from_masks(shape, masks, mode)