vkit.element.box

  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 Optional, Tuple, Union, Iterable
 15import math
 16import warnings
 17
 18import attrs
 19import numpy as np
 20from shapely.errors import ShapelyDeprecationWarning
 21from shapely.geometry import box as build_shapely_polygon_as_box
 22from shapely.strtree import STRtree
 23
 24from .type import Shapable, ElementSetOperationMode
 25from .opt import (
 26    clip_val,
 27    resize_val,
 28    extract_shape_from_shapable_or_shape,
 29    generate_shape_and_resized_shape,
 30    fill_np_array,
 31)
 32
 33# Shapely version has been explicitly locked under 2.0, hence ignore this warning.
 34warnings.filterwarnings('ignore', category=ShapelyDeprecationWarning)
 35
 36
 37@attrs.define(frozen=True)
 38class Box(Shapable):
 39    # By design, smooth positioning is not supported in Box.
 40
 41    up: int
 42    down: int
 43    left: int
 44    right: int
 45
 46    ###############
 47    # Constructor #
 48    ###############
 49    @classmethod
 50    def from_shape(cls, shape: Tuple[int, int]):
 51        height, width = shape
 52        return cls(
 53            up=0,
 54            down=height - 1,
 55            left=0,
 56            right=width - 1,
 57        )
 58
 59    @classmethod
 60    def from_shapable(cls, shapable: Shapable):
 61        return cls.from_shape(shapable.shape)
 62
 63    @classmethod
 64    def from_boxes(cls, boxes: Iterable['Box']):
 65        # Build a bounding box.
 66        boxes_iter = iter(boxes)
 67
 68        first_box = next(boxes_iter)
 69        up = first_box.up
 70        down = first_box.down
 71        left = first_box.left
 72        right = first_box.right
 73
 74        for box in boxes_iter:
 75            up = min(up, box.up)
 76            down = max(down, box.down)
 77            left = min(left, box.left)
 78            right = max(right, box.right)
 79
 80        return cls(up=up, down=down, left=left, right=right)
 81
 82    ############
 83    # Property #
 84    ############
 85    @property
 86    def height(self):
 87        return self.down + 1 - self.up
 88
 89    @property
 90    def width(self):
 91        return self.right + 1 - self.left
 92
 93    @property
 94    def valid(self):
 95        return (0 <= self.up <= self.down) and (0 <= self.left <= self.right)
 96
 97    ##############
 98    # Conversion #
 99    ##############
100    def to_polygon(self, step: Optional[int] = None):
101        if self.up == self.down or self.left == self.right:
102            raise RuntimeError(f'Cannot convert box={self} to polygon.')
103
104        # NOTE: Up left -> up right -> down right -> down left
105        # Char-level labelings are generated based on this ordering.
106        if step is None:
107            points = PointTuple.from_xy_pairs((
108                (self.left, self.up),
109                (self.right, self.up),
110                (self.right, self.down),
111                (self.left, self.down),
112            ))
113
114        else:
115            assert step > 0
116
117            xs = list(range(self.left, self.right + 1, step))
118            if xs[-1] < self.right:
119                xs.append(self.right)
120
121            ys = list(range(self.up, self.down + 1, step))
122            if ys[-1] == self.down:
123                # NOTE: check first to avoid oob error.
124                ys.pop()
125            ys.pop(0)
126
127            points = PointList()
128            # Up.
129            for x in xs:
130                points.append(Point.create(y=self.up, x=x))
131            # Right.
132            for y in ys:
133                points.append(Point.create(y=y, x=self.right))
134            # Down.
135            for x in reversed(xs):
136                points.append(Point.create(y=self.down, x=x))
137            # Left.
138            for y in reversed(ys):
139                points.append(Point.create(y=y, x=self.left))
140
141        return Polygon.create(points=points)
142
143    def to_shapely_polygon(self):
144        return build_shapely_polygon_as_box(
145            miny=self.up,
146            maxy=self.down,
147            minx=self.left,
148            maxx=self.right,
149        )
150
151    ############
152    # Operator #
153    ############
154    def get_center_point(self):
155        return Point.create(y=(self.up + self.down) / 2, x=(self.left + self.right) / 2)
156
157    def to_clipped_box(self, shapable_or_shape: Union[Shapable, Tuple[int, int]]):
158        height, width = extract_shape_from_shapable_or_shape(shapable_or_shape)
159        return Box(
160            up=clip_val(self.up, height),
161            down=clip_val(self.down, height),
162            left=clip_val(self.left, width),
163            right=clip_val(self.right, width),
164        )
165
166    def to_conducted_resized_box(
167        self,
168        shapable_or_shape: Union[Shapable, Tuple[int, int]],
169        resized_height: Optional[int] = None,
170        resized_width: Optional[int] = None,
171    ):
172        (
173            height,
174            width,
175            resized_height,
176            resized_width,
177        ) = generate_shape_and_resized_shape(
178            shapable_or_shape=shapable_or_shape,
179            resized_height=resized_height,
180            resized_width=resized_width
181        )
182        return Box(
183            up=round(resize_val(self.up, height, resized_height)),
184            down=round(resize_val(self.down, height, resized_height)),
185            left=round(resize_val(self.left, width, resized_width)),
186            right=round(resize_val(self.right, width, resized_width)),
187        )
188
189    def to_resized_box(
190        self,
191        resized_height: Optional[int] = None,
192        resized_width: Optional[int] = None,
193    ):
194        return self.to_conducted_resized_box(
195            shapable_or_shape=self,
196            resized_height=resized_height,
197            resized_width=resized_width,
198        )
199
200    def to_shifted_box(self, offset_y: int = 0, offset_x: int = 0):
201        return Box(
202            up=self.up + offset_y,
203            down=self.down + offset_y,
204            left=self.left + offset_x,
205            right=self.right + offset_x,
206        )
207
208    def to_relative_box(self, origin_y: int, origin_x: int):
209        return self.to_shifted_box(offset_y=-origin_y, offset_x=-origin_x)
210
211    def to_dilated_box(self, ratio: float, clip_long_side: bool = False):
212        expand_vert = math.ceil(self.height * ratio / 2)
213        expand_hori = math.ceil(self.width * ratio / 2)
214
215        if clip_long_side:
216            expand_min = min(expand_vert, expand_hori)
217            expand_vert = expand_min
218            expand_hori = expand_min
219
220        return Box(
221            up=self.up - expand_vert,
222            down=self.down + expand_vert,
223            left=self.left - expand_hori,
224            right=self.right + expand_hori,
225        )
226
227    def get_boxes_for_box_attached_opt(self, element_box: Optional['Box']):
228        if element_box is None:
229            relative_box = self
230            new_element_box = None
231
232        else:
233            assert element_box.up <= self.up <= self.down <= element_box.down
234            assert element_box.left <= self.left <= self.right <= element_box.right
235
236            # NOTE: Some shape, implicitly.
237            relative_box = self.to_relative_box(
238                origin_y=element_box.up,
239                origin_x=element_box.left,
240            )
241            new_element_box = self
242
243        return relative_box, new_element_box
244
245    def extract_np_array(self, mat: np.ndarray) -> np.ndarray:
246        assert 0 <= self.up <= self.down <= mat.shape[0]
247        assert 0 <= self.left <= self.right <= mat.shape[1]
248        return mat[self.up:self.down + 1, self.left:self.right + 1]
249
250    def extract_mask(self, mask: 'Mask'):
251        relative_box, new_mask_box = self.get_boxes_for_box_attached_opt(mask.box)
252
253        if relative_box.shape == mask.shape:
254            return mask
255
256        return attrs.evolve(
257            mask,
258            mat=relative_box.extract_np_array(mask.mat),
259            box=new_mask_box,
260        )
261
262    def extract_score_map(self, score_map: 'ScoreMap'):
263        relative_box, new_score_map_box = self.get_boxes_for_box_attached_opt(score_map.box)
264
265        if relative_box.shape == score_map.shape:
266            return score_map
267
268        return attrs.evolve(
269            score_map,
270            mat=relative_box.extract_np_array(score_map.mat),
271            box=new_score_map_box,
272        )
273
274    def extract_image(self, image: 'Image'):
275        relative_box, new_image_box = self.get_boxes_for_box_attached_opt(image.box)
276
277        if relative_box.shape == image.shape:
278            return image
279
280        return attrs.evolve(
281            image,
282            mat=relative_box.extract_np_array(image.mat),
283            box=new_image_box,
284        )
285
286    def prep_mat_and_value(
287        self,
288        mat: np.ndarray,
289        value: Union[np.ndarray, Tuple[float, ...], float],
290    ):
291        mat_shape_before_extraction = (mat.shape[0], mat.shape[1])
292        if mat_shape_before_extraction != self.shape:
293            mat = self.extract_np_array(mat)
294
295        if isinstance(value, np.ndarray):
296            value_shape_before_extraction = (value.shape[0], value.shape[1])
297            if value_shape_before_extraction != (mat.shape[0], mat.shape[1]):
298                assert value_shape_before_extraction == mat_shape_before_extraction
299                value = self.extract_np_array(value)
300
301            if value.dtype != mat.dtype:
302                value = value.astype(mat.dtype)
303
304        return mat, value
305
306    @classmethod
307    def get_np_mask_from_element_mask(cls, element_mask: Optional[Union['Mask', np.ndarray]]):
308        np_mask = None
309        if element_mask:
310            if isinstance(element_mask, Mask):
311                # NOTE: Mask.box is ignored.
312                np_mask = element_mask.np_mask
313            else:
314                np_mask = element_mask
315        return np_mask
316
317    def fill_np_array(
318        self,
319        mat: np.ndarray,
320        value: Union[np.ndarray, Tuple[float, ...], float],
321        np_mask: Optional[np.ndarray] = None,
322        alpha: Union['ScoreMap', np.ndarray, float] = 1.0,
323        keep_max_value: bool = False,
324        keep_min_value: bool = False,
325    ):
326        mat, value = self.prep_mat_and_value(mat, value)
327
328        if isinstance(alpha, ScoreMap):
329            # NOTE:
330            # 1. Place before np_mask to simplify ScoreMap opts.
331            # 2. ScoreMap.box is ignored.
332            assert alpha.is_prob
333            alpha = alpha.mat
334
335        if np_mask is None and isinstance(alpha, np.ndarray):
336            # For optimizing sparse alpha matrix.
337            np_mask = (alpha > 0.0)
338
339        fill_np_array(
340            mat=mat,
341            value=value,
342            np_mask=np_mask,
343            alpha=alpha,
344            keep_max_value=keep_max_value,
345            keep_min_value=keep_min_value,
346        )
347
348    def fill_mask(
349        self,
350        mask: 'Mask',
351        value: Union['Mask', np.ndarray, int] = 1,
352        mask_mask: Optional[Union['Mask', np.ndarray]] = None,
353        keep_max_value: bool = False,
354        keep_min_value: bool = False,
355    ):
356        relative_box, _ = self.get_boxes_for_box_attached_opt(mask.box)
357
358        if isinstance(value, Mask):
359            if value.shape != self.shape:
360                value = self.extract_mask(value)
361            value = value.mat
362
363        np_mask = self.get_np_mask_from_element_mask(mask_mask)
364
365        with mask.writable_context:
366            relative_box.fill_np_array(
367                mask.mat,
368                value,
369                np_mask=np_mask,
370                keep_max_value=keep_max_value,
371                keep_min_value=keep_min_value,
372            )
373
374    def fill_score_map(
375        self,
376        score_map: 'ScoreMap',
377        value: Union['ScoreMap', np.ndarray, float],
378        score_map_mask: Optional[Union['Mask', np.ndarray]] = None,
379        keep_max_value: bool = False,
380        keep_min_value: bool = False,
381    ):
382        relative_box, _ = self.get_boxes_for_box_attached_opt(score_map.box)
383
384        if isinstance(value, ScoreMap):
385            if value.shape != self.shape:
386                value = self.extract_score_map(value)
387            value = value.mat
388
389        np_mask = self.get_np_mask_from_element_mask(score_map_mask)
390
391        with score_map.writable_context:
392            relative_box.fill_np_array(
393                score_map.mat,
394                value,
395                np_mask=np_mask,
396                keep_max_value=keep_max_value,
397                keep_min_value=keep_min_value,
398            )
399
400    def fill_image(
401        self,
402        image: 'Image',
403        value: Union['Image', np.ndarray, Tuple[int, ...], int],
404        image_mask: Optional[Union['Mask', np.ndarray]] = None,
405        alpha: Union['ScoreMap', np.ndarray, float] = 1.0,
406    ):
407        relative_box, _ = self.get_boxes_for_box_attached_opt(image.box)
408
409        if isinstance(value, Image):
410            if value.shape != self.shape:
411                value = self.extract_image(value)
412            value = value.mat
413
414        np_mask = self.get_np_mask_from_element_mask(image_mask)
415
416        with image.writable_context:
417            relative_box.fill_np_array(
418                image.mat,
419                value,
420                np_mask=np_mask,
421                alpha=alpha,
422            )
423
424
425class BoxOverlappingValidator:
426
427    def __init__(self, boxes: Iterable[Box]):
428        self.strtree = STRtree(box.to_shapely_polygon() for box in boxes)
429
430    def is_overlapped(self, box: Box):
431        shapely_polygon = box.to_shapely_polygon()
432        for _ in self.strtree.query(shapely_polygon):
433            # NOTE: No need to test intersection since the extent of a box is itself.
434            return True
435        return False
436
437
438def generate_fill_by_boxes_mask(
439    shape: Tuple[int, int],
440    boxes: Iterable[Box],
441    mode: ElementSetOperationMode,
442):
443    if mode == ElementSetOperationMode.UNION:
444        return None
445    else:
446        return Mask.from_boxes(shape, boxes, mode)
447
448
449# Cyclic dependency, by design.
450from .point import Point, PointList, PointTuple  # noqa: E402
451from .polygon import Polygon  # noqa: E402
452from .mask import Mask  # noqa: E402
453from .score_map import ScoreMap  # noqa: E402
454from .image import Image  # noqa: E402
class Box(vkit.element.type.Shapable):
 39class Box(Shapable):
 40    # By design, smooth positioning is not supported in Box.
 41
 42    up: int
 43    down: int
 44    left: int
 45    right: int
 46
 47    ###############
 48    # Constructor #
 49    ###############
 50    @classmethod
 51    def from_shape(cls, shape: Tuple[int, int]):
 52        height, width = shape
 53        return cls(
 54            up=0,
 55            down=height - 1,
 56            left=0,
 57            right=width - 1,
 58        )
 59
 60    @classmethod
 61    def from_shapable(cls, shapable: Shapable):
 62        return cls.from_shape(shapable.shape)
 63
 64    @classmethod
 65    def from_boxes(cls, boxes: Iterable['Box']):
 66        # Build a bounding box.
 67        boxes_iter = iter(boxes)
 68
 69        first_box = next(boxes_iter)
 70        up = first_box.up
 71        down = first_box.down
 72        left = first_box.left
 73        right = first_box.right
 74
 75        for box in boxes_iter:
 76            up = min(up, box.up)
 77            down = max(down, box.down)
 78            left = min(left, box.left)
 79            right = max(right, box.right)
 80
 81        return cls(up=up, down=down, left=left, right=right)
 82
 83    ############
 84    # Property #
 85    ############
 86    @property
 87    def height(self):
 88        return self.down + 1 - self.up
 89
 90    @property
 91    def width(self):
 92        return self.right + 1 - self.left
 93
 94    @property
 95    def valid(self):
 96        return (0 <= self.up <= self.down) and (0 <= self.left <= self.right)
 97
 98    ##############
 99    # Conversion #
100    ##############
101    def to_polygon(self, step: Optional[int] = None):
102        if self.up == self.down or self.left == self.right:
103            raise RuntimeError(f'Cannot convert box={self} to polygon.')
104
105        # NOTE: Up left -> up right -> down right -> down left
106        # Char-level labelings are generated based on this ordering.
107        if step is None:
108            points = PointTuple.from_xy_pairs((
109                (self.left, self.up),
110                (self.right, self.up),
111                (self.right, self.down),
112                (self.left, self.down),
113            ))
114
115        else:
116            assert step > 0
117
118            xs = list(range(self.left, self.right + 1, step))
119            if xs[-1] < self.right:
120                xs.append(self.right)
121
122            ys = list(range(self.up, self.down + 1, step))
123            if ys[-1] == self.down:
124                # NOTE: check first to avoid oob error.
125                ys.pop()
126            ys.pop(0)
127
128            points = PointList()
129            # Up.
130            for x in xs:
131                points.append(Point.create(y=self.up, x=x))
132            # Right.
133            for y in ys:
134                points.append(Point.create(y=y, x=self.right))
135            # Down.
136            for x in reversed(xs):
137                points.append(Point.create(y=self.down, x=x))
138            # Left.
139            for y in reversed(ys):
140                points.append(Point.create(y=y, x=self.left))
141
142        return Polygon.create(points=points)
143
144    def to_shapely_polygon(self):
145        return build_shapely_polygon_as_box(
146            miny=self.up,
147            maxy=self.down,
148            minx=self.left,
149            maxx=self.right,
150        )
151
152    ############
153    # Operator #
154    ############
155    def get_center_point(self):
156        return Point.create(y=(self.up + self.down) / 2, x=(self.left + self.right) / 2)
157
158    def to_clipped_box(self, shapable_or_shape: Union[Shapable, Tuple[int, int]]):
159        height, width = extract_shape_from_shapable_or_shape(shapable_or_shape)
160        return Box(
161            up=clip_val(self.up, height),
162            down=clip_val(self.down, height),
163            left=clip_val(self.left, width),
164            right=clip_val(self.right, width),
165        )
166
167    def to_conducted_resized_box(
168        self,
169        shapable_or_shape: Union[Shapable, Tuple[int, int]],
170        resized_height: Optional[int] = None,
171        resized_width: Optional[int] = None,
172    ):
173        (
174            height,
175            width,
176            resized_height,
177            resized_width,
178        ) = generate_shape_and_resized_shape(
179            shapable_or_shape=shapable_or_shape,
180            resized_height=resized_height,
181            resized_width=resized_width
182        )
183        return Box(
184            up=round(resize_val(self.up, height, resized_height)),
185            down=round(resize_val(self.down, height, resized_height)),
186            left=round(resize_val(self.left, width, resized_width)),
187            right=round(resize_val(self.right, width, resized_width)),
188        )
189
190    def to_resized_box(
191        self,
192        resized_height: Optional[int] = None,
193        resized_width: Optional[int] = None,
194    ):
195        return self.to_conducted_resized_box(
196            shapable_or_shape=self,
197            resized_height=resized_height,
198            resized_width=resized_width,
199        )
200
201    def to_shifted_box(self, offset_y: int = 0, offset_x: int = 0):
202        return Box(
203            up=self.up + offset_y,
204            down=self.down + offset_y,
205            left=self.left + offset_x,
206            right=self.right + offset_x,
207        )
208
209    def to_relative_box(self, origin_y: int, origin_x: int):
210        return self.to_shifted_box(offset_y=-origin_y, offset_x=-origin_x)
211
212    def to_dilated_box(self, ratio: float, clip_long_side: bool = False):
213        expand_vert = math.ceil(self.height * ratio / 2)
214        expand_hori = math.ceil(self.width * ratio / 2)
215
216        if clip_long_side:
217            expand_min = min(expand_vert, expand_hori)
218            expand_vert = expand_min
219            expand_hori = expand_min
220
221        return Box(
222            up=self.up - expand_vert,
223            down=self.down + expand_vert,
224            left=self.left - expand_hori,
225            right=self.right + expand_hori,
226        )
227
228    def get_boxes_for_box_attached_opt(self, element_box: Optional['Box']):
229        if element_box is None:
230            relative_box = self
231            new_element_box = None
232
233        else:
234            assert element_box.up <= self.up <= self.down <= element_box.down
235            assert element_box.left <= self.left <= self.right <= element_box.right
236
237            # NOTE: Some shape, implicitly.
238            relative_box = self.to_relative_box(
239                origin_y=element_box.up,
240                origin_x=element_box.left,
241            )
242            new_element_box = self
243
244        return relative_box, new_element_box
245
246    def extract_np_array(self, mat: np.ndarray) -> np.ndarray:
247        assert 0 <= self.up <= self.down <= mat.shape[0]
248        assert 0 <= self.left <= self.right <= mat.shape[1]
249        return mat[self.up:self.down + 1, self.left:self.right + 1]
250
251    def extract_mask(self, mask: 'Mask'):
252        relative_box, new_mask_box = self.get_boxes_for_box_attached_opt(mask.box)
253
254        if relative_box.shape == mask.shape:
255            return mask
256
257        return attrs.evolve(
258            mask,
259            mat=relative_box.extract_np_array(mask.mat),
260            box=new_mask_box,
261        )
262
263    def extract_score_map(self, score_map: 'ScoreMap'):
264        relative_box, new_score_map_box = self.get_boxes_for_box_attached_opt(score_map.box)
265
266        if relative_box.shape == score_map.shape:
267            return score_map
268
269        return attrs.evolve(
270            score_map,
271            mat=relative_box.extract_np_array(score_map.mat),
272            box=new_score_map_box,
273        )
274
275    def extract_image(self, image: 'Image'):
276        relative_box, new_image_box = self.get_boxes_for_box_attached_opt(image.box)
277
278        if relative_box.shape == image.shape:
279            return image
280
281        return attrs.evolve(
282            image,
283            mat=relative_box.extract_np_array(image.mat),
284            box=new_image_box,
285        )
286
287    def prep_mat_and_value(
288        self,
289        mat: np.ndarray,
290        value: Union[np.ndarray, Tuple[float, ...], float],
291    ):
292        mat_shape_before_extraction = (mat.shape[0], mat.shape[1])
293        if mat_shape_before_extraction != self.shape:
294            mat = self.extract_np_array(mat)
295
296        if isinstance(value, np.ndarray):
297            value_shape_before_extraction = (value.shape[0], value.shape[1])
298            if value_shape_before_extraction != (mat.shape[0], mat.shape[1]):
299                assert value_shape_before_extraction == mat_shape_before_extraction
300                value = self.extract_np_array(value)
301
302            if value.dtype != mat.dtype:
303                value = value.astype(mat.dtype)
304
305        return mat, value
306
307    @classmethod
308    def get_np_mask_from_element_mask(cls, element_mask: Optional[Union['Mask', np.ndarray]]):
309        np_mask = None
310        if element_mask:
311            if isinstance(element_mask, Mask):
312                # NOTE: Mask.box is ignored.
313                np_mask = element_mask.np_mask
314            else:
315                np_mask = element_mask
316        return np_mask
317
318    def fill_np_array(
319        self,
320        mat: np.ndarray,
321        value: Union[np.ndarray, Tuple[float, ...], float],
322        np_mask: Optional[np.ndarray] = None,
323        alpha: Union['ScoreMap', np.ndarray, float] = 1.0,
324        keep_max_value: bool = False,
325        keep_min_value: bool = False,
326    ):
327        mat, value = self.prep_mat_and_value(mat, value)
328
329        if isinstance(alpha, ScoreMap):
330            # NOTE:
331            # 1. Place before np_mask to simplify ScoreMap opts.
332            # 2. ScoreMap.box is ignored.
333            assert alpha.is_prob
334            alpha = alpha.mat
335
336        if np_mask is None and isinstance(alpha, np.ndarray):
337            # For optimizing sparse alpha matrix.
338            np_mask = (alpha > 0.0)
339
340        fill_np_array(
341            mat=mat,
342            value=value,
343            np_mask=np_mask,
344            alpha=alpha,
345            keep_max_value=keep_max_value,
346            keep_min_value=keep_min_value,
347        )
348
349    def fill_mask(
350        self,
351        mask: 'Mask',
352        value: Union['Mask', np.ndarray, int] = 1,
353        mask_mask: Optional[Union['Mask', np.ndarray]] = None,
354        keep_max_value: bool = False,
355        keep_min_value: bool = False,
356    ):
357        relative_box, _ = self.get_boxes_for_box_attached_opt(mask.box)
358
359        if isinstance(value, Mask):
360            if value.shape != self.shape:
361                value = self.extract_mask(value)
362            value = value.mat
363
364        np_mask = self.get_np_mask_from_element_mask(mask_mask)
365
366        with mask.writable_context:
367            relative_box.fill_np_array(
368                mask.mat,
369                value,
370                np_mask=np_mask,
371                keep_max_value=keep_max_value,
372                keep_min_value=keep_min_value,
373            )
374
375    def fill_score_map(
376        self,
377        score_map: 'ScoreMap',
378        value: Union['ScoreMap', np.ndarray, float],
379        score_map_mask: Optional[Union['Mask', np.ndarray]] = None,
380        keep_max_value: bool = False,
381        keep_min_value: bool = False,
382    ):
383        relative_box, _ = self.get_boxes_for_box_attached_opt(score_map.box)
384
385        if isinstance(value, ScoreMap):
386            if value.shape != self.shape:
387                value = self.extract_score_map(value)
388            value = value.mat
389
390        np_mask = self.get_np_mask_from_element_mask(score_map_mask)
391
392        with score_map.writable_context:
393            relative_box.fill_np_array(
394                score_map.mat,
395                value,
396                np_mask=np_mask,
397                keep_max_value=keep_max_value,
398                keep_min_value=keep_min_value,
399            )
400
401    def fill_image(
402        self,
403        image: 'Image',
404        value: Union['Image', np.ndarray, Tuple[int, ...], int],
405        image_mask: Optional[Union['Mask', np.ndarray]] = None,
406        alpha: Union['ScoreMap', np.ndarray, float] = 1.0,
407    ):
408        relative_box, _ = self.get_boxes_for_box_attached_opt(image.box)
409
410        if isinstance(value, Image):
411            if value.shape != self.shape:
412                value = self.extract_image(value)
413            value = value.mat
414
415        np_mask = self.get_np_mask_from_element_mask(image_mask)
416
417        with image.writable_context:
418            relative_box.fill_np_array(
419                image.mat,
420                value,
421                np_mask=np_mask,
422                alpha=alpha,
423            )
Box(up: int, down: int, left: int, right: int)
2def __init__(self, up, down, left, right):
3    _setattr(self, 'up', up)
4    _setattr(self, 'down', down)
5    _setattr(self, 'left', left)
6    _setattr(self, 'right', right)

Method generated by attrs for class Box.

@classmethod
def from_shape(cls, shape: Tuple[int, int]):
50    @classmethod
51    def from_shape(cls, shape: Tuple[int, int]):
52        height, width = shape
53        return cls(
54            up=0,
55            down=height - 1,
56            left=0,
57            right=width - 1,
58        )
@classmethod
def from_shapable(cls, shapable: vkit.element.type.Shapable):
60    @classmethod
61    def from_shapable(cls, shapable: Shapable):
62        return cls.from_shape(shapable.shape)
@classmethod
def from_boxes(cls, boxes: collections.abc.Iterable[vkit.element.box.Box]):
64    @classmethod
65    def from_boxes(cls, boxes: Iterable['Box']):
66        # Build a bounding box.
67        boxes_iter = iter(boxes)
68
69        first_box = next(boxes_iter)
70        up = first_box.up
71        down = first_box.down
72        left = first_box.left
73        right = first_box.right
74
75        for box in boxes_iter:
76            up = min(up, box.up)
77            down = max(down, box.down)
78            left = min(left, box.left)
79            right = max(right, box.right)
80
81        return cls(up=up, down=down, left=left, right=right)
def to_polygon(self, step: Union[int, NoneType] = None):
101    def to_polygon(self, step: Optional[int] = None):
102        if self.up == self.down or self.left == self.right:
103            raise RuntimeError(f'Cannot convert box={self} to polygon.')
104
105        # NOTE: Up left -> up right -> down right -> down left
106        # Char-level labelings are generated based on this ordering.
107        if step is None:
108            points = PointTuple.from_xy_pairs((
109                (self.left, self.up),
110                (self.right, self.up),
111                (self.right, self.down),
112                (self.left, self.down),
113            ))
114
115        else:
116            assert step > 0
117
118            xs = list(range(self.left, self.right + 1, step))
119            if xs[-1] < self.right:
120                xs.append(self.right)
121
122            ys = list(range(self.up, self.down + 1, step))
123            if ys[-1] == self.down:
124                # NOTE: check first to avoid oob error.
125                ys.pop()
126            ys.pop(0)
127
128            points = PointList()
129            # Up.
130            for x in xs:
131                points.append(Point.create(y=self.up, x=x))
132            # Right.
133            for y in ys:
134                points.append(Point.create(y=y, x=self.right))
135            # Down.
136            for x in reversed(xs):
137                points.append(Point.create(y=self.down, x=x))
138            # Left.
139            for y in reversed(ys):
140                points.append(Point.create(y=y, x=self.left))
141
142        return Polygon.create(points=points)
def to_shapely_polygon(self):
144    def to_shapely_polygon(self):
145        return build_shapely_polygon_as_box(
146            miny=self.up,
147            maxy=self.down,
148            minx=self.left,
149            maxx=self.right,
150        )
def get_center_point(self):
155    def get_center_point(self):
156        return Point.create(y=(self.up + self.down) / 2, x=(self.left + self.right) / 2)
def to_clipped_box( self, shapable_or_shape: Union[vkit.element.type.Shapable, Tuple[int, int]]):
158    def to_clipped_box(self, shapable_or_shape: Union[Shapable, Tuple[int, int]]):
159        height, width = extract_shape_from_shapable_or_shape(shapable_or_shape)
160        return Box(
161            up=clip_val(self.up, height),
162            down=clip_val(self.down, height),
163            left=clip_val(self.left, width),
164            right=clip_val(self.right, width),
165        )
def to_conducted_resized_box( self, shapable_or_shape: Union[vkit.element.type.Shapable, Tuple[int, int]], resized_height: Union[int, NoneType] = None, resized_width: Union[int, NoneType] = None):
167    def to_conducted_resized_box(
168        self,
169        shapable_or_shape: Union[Shapable, Tuple[int, int]],
170        resized_height: Optional[int] = None,
171        resized_width: Optional[int] = None,
172    ):
173        (
174            height,
175            width,
176            resized_height,
177            resized_width,
178        ) = generate_shape_and_resized_shape(
179            shapable_or_shape=shapable_or_shape,
180            resized_height=resized_height,
181            resized_width=resized_width
182        )
183        return Box(
184            up=round(resize_val(self.up, height, resized_height)),
185            down=round(resize_val(self.down, height, resized_height)),
186            left=round(resize_val(self.left, width, resized_width)),
187            right=round(resize_val(self.right, width, resized_width)),
188        )
def to_resized_box( self, resized_height: Union[int, NoneType] = None, resized_width: Union[int, NoneType] = None):
190    def to_resized_box(
191        self,
192        resized_height: Optional[int] = None,
193        resized_width: Optional[int] = None,
194    ):
195        return self.to_conducted_resized_box(
196            shapable_or_shape=self,
197            resized_height=resized_height,
198            resized_width=resized_width,
199        )
def to_shifted_box(self, offset_y: int = 0, offset_x: int = 0):
201    def to_shifted_box(self, offset_y: int = 0, offset_x: int = 0):
202        return Box(
203            up=self.up + offset_y,
204            down=self.down + offset_y,
205            left=self.left + offset_x,
206            right=self.right + offset_x,
207        )
def to_relative_box(self, origin_y: int, origin_x: int):
209    def to_relative_box(self, origin_y: int, origin_x: int):
210        return self.to_shifted_box(offset_y=-origin_y, offset_x=-origin_x)
def to_dilated_box(self, ratio: float, clip_long_side: bool = False):
212    def to_dilated_box(self, ratio: float, clip_long_side: bool = False):
213        expand_vert = math.ceil(self.height * ratio / 2)
214        expand_hori = math.ceil(self.width * ratio / 2)
215
216        if clip_long_side:
217            expand_min = min(expand_vert, expand_hori)
218            expand_vert = expand_min
219            expand_hori = expand_min
220
221        return Box(
222            up=self.up - expand_vert,
223            down=self.down + expand_vert,
224            left=self.left - expand_hori,
225            right=self.right + expand_hori,
226        )
def get_boxes_for_box_attached_opt(self, element_box: Union[vkit.element.box.Box, NoneType]):
228    def get_boxes_for_box_attached_opt(self, element_box: Optional['Box']):
229        if element_box is None:
230            relative_box = self
231            new_element_box = None
232
233        else:
234            assert element_box.up <= self.up <= self.down <= element_box.down
235            assert element_box.left <= self.left <= self.right <= element_box.right
236
237            # NOTE: Some shape, implicitly.
238            relative_box = self.to_relative_box(
239                origin_y=element_box.up,
240                origin_x=element_box.left,
241            )
242            new_element_box = self
243
244        return relative_box, new_element_box
def extract_np_array(self, mat: numpy.ndarray) -> numpy.ndarray:
246    def extract_np_array(self, mat: np.ndarray) -> np.ndarray:
247        assert 0 <= self.up <= self.down <= mat.shape[0]
248        assert 0 <= self.left <= self.right <= mat.shape[1]
249        return mat[self.up:self.down + 1, self.left:self.right + 1]
def extract_mask(self, mask: vkit.element.mask.Mask):
251    def extract_mask(self, mask: 'Mask'):
252        relative_box, new_mask_box = self.get_boxes_for_box_attached_opt(mask.box)
253
254        if relative_box.shape == mask.shape:
255            return mask
256
257        return attrs.evolve(
258            mask,
259            mat=relative_box.extract_np_array(mask.mat),
260            box=new_mask_box,
261        )
def extract_score_map(self, score_map: vkit.element.score_map.ScoreMap):
263    def extract_score_map(self, score_map: 'ScoreMap'):
264        relative_box, new_score_map_box = self.get_boxes_for_box_attached_opt(score_map.box)
265
266        if relative_box.shape == score_map.shape:
267            return score_map
268
269        return attrs.evolve(
270            score_map,
271            mat=relative_box.extract_np_array(score_map.mat),
272            box=new_score_map_box,
273        )
def extract_image(self, image: vkit.element.image.Image):
275    def extract_image(self, image: 'Image'):
276        relative_box, new_image_box = self.get_boxes_for_box_attached_opt(image.box)
277
278        if relative_box.shape == image.shape:
279            return image
280
281        return attrs.evolve(
282            image,
283            mat=relative_box.extract_np_array(image.mat),
284            box=new_image_box,
285        )
def prep_mat_and_value( self, mat: numpy.ndarray, value: Union[numpy.ndarray, Tuple[float, ...], float]):
287    def prep_mat_and_value(
288        self,
289        mat: np.ndarray,
290        value: Union[np.ndarray, Tuple[float, ...], float],
291    ):
292        mat_shape_before_extraction = (mat.shape[0], mat.shape[1])
293        if mat_shape_before_extraction != self.shape:
294            mat = self.extract_np_array(mat)
295
296        if isinstance(value, np.ndarray):
297            value_shape_before_extraction = (value.shape[0], value.shape[1])
298            if value_shape_before_extraction != (mat.shape[0], mat.shape[1]):
299                assert value_shape_before_extraction == mat_shape_before_extraction
300                value = self.extract_np_array(value)
301
302            if value.dtype != mat.dtype:
303                value = value.astype(mat.dtype)
304
305        return mat, value
@classmethod
def get_np_mask_from_element_mask( cls, element_mask: Union[vkit.element.mask.Mask, numpy.ndarray, NoneType]):
307    @classmethod
308    def get_np_mask_from_element_mask(cls, element_mask: Optional[Union['Mask', np.ndarray]]):
309        np_mask = None
310        if element_mask:
311            if isinstance(element_mask, Mask):
312                # NOTE: Mask.box is ignored.
313                np_mask = element_mask.np_mask
314            else:
315                np_mask = element_mask
316        return np_mask
def fill_np_array( self, mat: numpy.ndarray, value: Union[numpy.ndarray, Tuple[float, ...], float], np_mask: Union[numpy.ndarray, NoneType] = None, alpha: Union[vkit.element.score_map.ScoreMap, numpy.ndarray, float] = 1.0, keep_max_value: bool = False, keep_min_value: bool = False):
318    def fill_np_array(
319        self,
320        mat: np.ndarray,
321        value: Union[np.ndarray, Tuple[float, ...], float],
322        np_mask: Optional[np.ndarray] = None,
323        alpha: Union['ScoreMap', np.ndarray, float] = 1.0,
324        keep_max_value: bool = False,
325        keep_min_value: bool = False,
326    ):
327        mat, value = self.prep_mat_and_value(mat, value)
328
329        if isinstance(alpha, ScoreMap):
330            # NOTE:
331            # 1. Place before np_mask to simplify ScoreMap opts.
332            # 2. ScoreMap.box is ignored.
333            assert alpha.is_prob
334            alpha = alpha.mat
335
336        if np_mask is None and isinstance(alpha, np.ndarray):
337            # For optimizing sparse alpha matrix.
338            np_mask = (alpha > 0.0)
339
340        fill_np_array(
341            mat=mat,
342            value=value,
343            np_mask=np_mask,
344            alpha=alpha,
345            keep_max_value=keep_max_value,
346            keep_min_value=keep_min_value,
347        )
def fill_mask( self, mask: vkit.element.mask.Mask, value: Union[vkit.element.mask.Mask, numpy.ndarray, int] = 1, mask_mask: Union[vkit.element.mask.Mask, numpy.ndarray, NoneType] = None, keep_max_value: bool = False, keep_min_value: bool = False):
349    def fill_mask(
350        self,
351        mask: 'Mask',
352        value: Union['Mask', np.ndarray, int] = 1,
353        mask_mask: Optional[Union['Mask', np.ndarray]] = None,
354        keep_max_value: bool = False,
355        keep_min_value: bool = False,
356    ):
357        relative_box, _ = self.get_boxes_for_box_attached_opt(mask.box)
358
359        if isinstance(value, Mask):
360            if value.shape != self.shape:
361                value = self.extract_mask(value)
362            value = value.mat
363
364        np_mask = self.get_np_mask_from_element_mask(mask_mask)
365
366        with mask.writable_context:
367            relative_box.fill_np_array(
368                mask.mat,
369                value,
370                np_mask=np_mask,
371                keep_max_value=keep_max_value,
372                keep_min_value=keep_min_value,
373            )
def fill_score_map( self, score_map: vkit.element.score_map.ScoreMap, value: Union[vkit.element.score_map.ScoreMap, numpy.ndarray, float], score_map_mask: Union[vkit.element.mask.Mask, numpy.ndarray, NoneType] = None, keep_max_value: bool = False, keep_min_value: bool = False):
375    def fill_score_map(
376        self,
377        score_map: 'ScoreMap',
378        value: Union['ScoreMap', np.ndarray, float],
379        score_map_mask: Optional[Union['Mask', np.ndarray]] = None,
380        keep_max_value: bool = False,
381        keep_min_value: bool = False,
382    ):
383        relative_box, _ = self.get_boxes_for_box_attached_opt(score_map.box)
384
385        if isinstance(value, ScoreMap):
386            if value.shape != self.shape:
387                value = self.extract_score_map(value)
388            value = value.mat
389
390        np_mask = self.get_np_mask_from_element_mask(score_map_mask)
391
392        with score_map.writable_context:
393            relative_box.fill_np_array(
394                score_map.mat,
395                value,
396                np_mask=np_mask,
397                keep_max_value=keep_max_value,
398                keep_min_value=keep_min_value,
399            )
def fill_image( self, image: vkit.element.image.Image, value: Union[vkit.element.image.Image, numpy.ndarray, Tuple[int, ...], int], image_mask: Union[vkit.element.mask.Mask, numpy.ndarray, NoneType] = None, alpha: Union[vkit.element.score_map.ScoreMap, numpy.ndarray, float] = 1.0):
401    def fill_image(
402        self,
403        image: 'Image',
404        value: Union['Image', np.ndarray, Tuple[int, ...], int],
405        image_mask: Optional[Union['Mask', np.ndarray]] = None,
406        alpha: Union['ScoreMap', np.ndarray, float] = 1.0,
407    ):
408        relative_box, _ = self.get_boxes_for_box_attached_opt(image.box)
409
410        if isinstance(value, Image):
411            if value.shape != self.shape:
412                value = self.extract_image(value)
413            value = value.mat
414
415        np_mask = self.get_np_mask_from_element_mask(image_mask)
416
417        with image.writable_context:
418            relative_box.fill_np_array(
419                image.mat,
420                value,
421                np_mask=np_mask,
422                alpha=alpha,
423            )
class BoxOverlappingValidator:
426class BoxOverlappingValidator:
427
428    def __init__(self, boxes: Iterable[Box]):
429        self.strtree = STRtree(box.to_shapely_polygon() for box in boxes)
430
431    def is_overlapped(self, box: Box):
432        shapely_polygon = box.to_shapely_polygon()
433        for _ in self.strtree.query(shapely_polygon):
434            # NOTE: No need to test intersection since the extent of a box is itself.
435            return True
436        return False
BoxOverlappingValidator(boxes: Iterable[vkit.element.box.Box])
428    def __init__(self, boxes: Iterable[Box]):
429        self.strtree = STRtree(box.to_shapely_polygon() for box in boxes)
def is_overlapped(self, box: vkit.element.box.Box):
431    def is_overlapped(self, box: Box):
432        shapely_polygon = box.to_shapely_polygon()
433        for _ in self.strtree.query(shapely_polygon):
434            # NOTE: No need to test intersection since the extent of a box is itself.
435            return True
436        return False
def generate_fill_by_boxes_mask( shape: Tuple[int, int], boxes: Iterable[vkit.element.box.Box], mode: vkit.element.type.ElementSetOperationMode):
439def generate_fill_by_boxes_mask(
440    shape: Tuple[int, int],
441    boxes: Iterable[Box],
442    mode: ElementSetOperationMode,
443):
444    if mode == ElementSetOperationMode.UNION:
445        return None
446    else:
447        return Mask.from_boxes(shape, boxes, mode)