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

Method generated by attrs for class Box.

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