vkit.element.score_map

  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, Callable, List, TypeVar
 15from contextlib import ContextDecorator
 16
 17import attrs
 18import numpy as np
 19import cv2 as cv
 20
 21from .type import Shapable, ElementSetOperationMode
 22from .opt import generate_shape_and_resized_shape
 23
 24
 25@attrs.define
 26class ScoreMapSetItemConfig:
 27    value: Union[
 28        'ScoreMap',
 29        np.ndarray,
 30        float,
 31    ] = 1.0  # yapf: disable
 32    keep_max_value: bool = False
 33    keep_min_value: bool = False
 34
 35
 36@attrs.define
 37class NpVec:
 38    x: np.ndarray
 39    y: np.ndarray
 40
 41    @classmethod
 42    def from_point(cls, point: 'Point'):
 43        return cls(
 44            x=np.asarray(point.smooth_x, dtype=np.float32),
 45            y=np.asarray(point.smooth_y, dtype=np.float32),
 46        )
 47
 48    def __add__(self, other: 'NpVec'):
 49        return NpVec(x=self.x + other.x, y=self.y + other.y)
 50
 51    def __sub__(self, other: 'NpVec'):
 52        return NpVec(x=self.x - other.x, y=self.y - other.y)  # type: ignore
 53
 54    def __mul__(self, other: 'NpVec') -> np.ndarray:
 55        return self.x * other.y - self.y * other.x  # type: ignore
 56
 57
 58class WritableScoreMapContextDecorator(ContextDecorator):
 59
 60    def __init__(self, score_map: 'ScoreMap'):
 61        super().__init__()
 62        self.score_map = score_map
 63
 64    def __enter__(self):
 65        if self.score_map.mat.flags.c_contiguous:
 66            assert not self.score_map.mat.flags.writeable
 67
 68        try:
 69            self.score_map.mat.flags.writeable = True
 70        except ValueError:
 71            # Copy on write.
 72            object.__setattr__(
 73                self.score_map,
 74                'mat',
 75                np.array(self.score_map.mat),
 76            )
 77            assert self.score_map.mat.flags.writeable
 78
 79    def __exit__(self, *exc):  # type: ignore
 80        self.score_map.mat.flags.writeable = False
 81
 82
 83_E = TypeVar('_E', 'Box', 'Polygon', 'Mask')
 84
 85
 86@attrs.define(frozen=True, eq=False)
 87class ScoreMap(Shapable):
 88    mat: np.ndarray
 89    box: Optional['Box'] = None
 90    is_prob: bool = True
 91
 92    def __attrs_post_init__(self):
 93        if self.mat.ndim != 2:
 94            raise RuntimeError('ndim should == 2.')
 95        if self.box and self.shape != self.box.shape:
 96            raise RuntimeError('self.shape != box.shape.')
 97
 98        if self.mat.dtype != np.float32:
 99            raise RuntimeError('mat.dtype != np.float32')
100
101        # For the control of write.
102        self.mat.flags.writeable = False
103
104        if self.is_prob:
105            score_min = self.mat.min()
106            score_max = self.mat.max()
107            if score_min < 0.0 or score_max > 1.0:
108                raise RuntimeError('score not in range [0.0, 1.0]')
109
110    ###############
111    # Constructor #
112    ###############
113    @classmethod
114    def from_shape(
115        cls,
116        shape: Tuple[int, int],
117        value: float = 0.0,
118        is_prob: bool = True,
119    ):
120        height, width = shape
121        if is_prob:
122            assert 0.0 <= value <= 1.0
123        mat = np.full((height, width), fill_value=value, dtype=np.float32)
124        return cls(mat=mat, is_prob=is_prob)
125
126    @classmethod
127    def from_shapable(
128        cls,
129        shapable: Shapable,
130        value: float = 0.0,
131        is_prob: bool = True,
132    ):
133        return cls.from_shape(
134            shape=shapable.shape,
135            value=value,
136            is_prob=is_prob,
137        )
138
139    @classmethod
140    def from_quad_interpolation(
141        cls,
142        point0: 'Point',
143        point1: 'Point',
144        point2: 'Point',
145        point3: 'Point',
146        func_np_uv_to_mat: Callable[[np.ndarray], np.ndarray],
147        is_prob: bool = True,
148    ):
149        '''
150        Ref: https://www.reedbeta.com/blog/quadrilateral-interpolation-part-2/
151
152        points in clockwise order:
153        point0 0.0 -- u --> 1.0 point1
154                                 0.0
155                                  ↓
156                                  v
157                                  ↓
158                                 1.0
159        point3 0.0 <-- u -- 1.0 point2
160
161        lerp(a, b, r) = a + r * (b - a)
162        pointx(u, v) = lerp(
163            lerp(point0, point1, u),
164            lerp(point3, point2, u),
165            v,
166        )
167
168        Hence,
169        u in [0.0, 1.0], and
170        u -> 0.0, x -> line (point0, point3)
171        u -> 1.0, x -> line (point1, point2)
172
173        v in [0.0, 1.0], and
174        v -> 0.0, x -> line (point0, point1)
175        v -> 1.0, x -> line (point3, point2)
176        '''
177        polygon = Polygon.create((
178            point0,
179            point1,
180            point2,
181            point3,
182        ))
183        bounding_box = polygon.bounding_box
184        self_relative_polygon = polygon.self_relative_polygon
185        np_active_mask = polygon.internals.np_mask
186
187        np_vec_0 = NpVec.from_point(self_relative_polygon.points[0])
188        np_vec_1 = NpVec.from_point(self_relative_polygon.points[1])
189        np_vec_2 = NpVec.from_point(self_relative_polygon.points[2])
190        np_vec_3 = NpVec.from_point(self_relative_polygon.points[3])
191
192        # F(x, y) -> x
193        np_pointx_x = np.repeat(
194            np.expand_dims(
195                np.arange(bounding_box.width, dtype=np.int32),
196                axis=0,
197            ),
198            bounding_box.height,
199            axis=0,
200        )
201        # F(x, y) -> y
202        np_pointx_y = np.repeat(
203            np.expand_dims(
204                np.arange(bounding_box.height, dtype=np.int32),
205                axis=1,
206            ),
207            bounding_box.width,
208            axis=1,
209        )
210        np_vec_x = NpVec(x=np_pointx_x, y=np_pointx_y)
211
212        np_vec_q = np_vec_x - np_vec_0
213        np_vec_b1 = np_vec_1 - np_vec_0
214        np_vec_b2 = np_vec_3 - np_vec_0
215        np_vec_b3 = ((np_vec_0 - np_vec_1) - np_vec_3) + np_vec_2
216
217        scale_a = float(np_vec_b2 * np_vec_b3)
218
219        np_b: np.ndarray = (np_vec_b3 * np_vec_q) - (np_vec_b1 * np_vec_b2)  # type: ignore
220        np_b = np_b.astype(np.float32)
221
222        np_c = np_vec_b1 * np_vec_q
223        np_c = np_c.astype(np.float32)
224
225        # Solve v.
226        if abs(scale_a) < 0.001:
227            np_v = -np_c / np_b
228
229        else:
230            np_discrim = np.sqrt(np.power(np_b, 2) - 4 * scale_a * np_c)
231            scale_i2a = 0.5 / scale_a
232            np_v_pos = (-np_b + np_discrim) * scale_i2a
233            np_v_neg: np.ndarray = (-np_b - np_discrim) * scale_i2a  # type: ignore
234
235            np_masked_v_pos: np.ndarray = np_v_pos[np_active_mask]
236            np_v_pos_valid = (0.0 <= np_masked_v_pos) & (np_masked_v_pos <= 1.0)
237
238            np_masked_v_neg: np.ndarray = np_v_neg[np_active_mask]
239            np_v_neg_valid = (0.0 <= np_masked_v_neg) & (np_masked_v_neg <= 1.0)
240
241            if np_v_pos_valid.sum() >= np_v_neg_valid.sum():
242                np_v = np_v_pos
243            else:
244                np_v = np_v_neg
245
246        np_v[~np_active_mask] = 0.0
247        np_v = np.clip(np_v, 0.0, 1.0)
248
249        # Solve u.
250        np_u = np.zeros_like(np_v)
251
252        np_denom_x: np.ndarray = np_vec_b1.x + np_vec_b3.x * np_v
253        np_denom_y: np.ndarray = np_vec_b1.y + np_vec_b3.y * np_v
254
255        np_denom_x_mask = (np.abs(np_denom_x) > np.abs(np_denom_y)) & (np_denom_x != 0.0)
256        if np_denom_x_mask.any():
257            np_q_x = np_vec_q.x
258            np_u[np_denom_x_mask] = (
259                (np_q_x[np_denom_x_mask] - np_vec_b2.x * np_v[np_denom_x_mask])
260                / np_denom_x[np_denom_x_mask]
261            )
262
263        np_denom_y_mask = (~np_denom_x_mask) & (np_denom_y != 0.0)
264        if np_denom_y_mask.any():
265            np_q_y = np_vec_q.y
266            np_u[np_denom_y_mask] = (
267                (np_q_y[np_denom_y_mask] - np_vec_b2.y * np_v[np_denom_y_mask])
268                / np_denom_y[np_denom_y_mask]
269            )
270
271        np_u[~np_active_mask] = 0.0
272        np_u = np.clip(np_u, 0.0, 1.0)
273
274        # Stack to (height, width, 2)
275        np_uv = np.stack((np_u, np_v), axis=-1)
276
277        # Mat.
278        mat = func_np_uv_to_mat(np_uv)
279        return cls(
280            mat=mat,
281            box=bounding_box,
282            is_prob=is_prob,
283        )
284
285    ############
286    # Property #
287    ############
288    @property
289    def height(self):
290        return self.mat.shape[0]
291
292    @property
293    def width(self):
294        return self.mat.shape[1]
295
296    @property
297    def equivalent_box(self):
298        return self.box or Box.from_shapable(self)
299
300    @property
301    def writable_context(self):
302        return WritableScoreMapContextDecorator(self)
303
304    ############
305    # Operator #
306    ############
307    def copy(self):
308        return attrs.evolve(self, mat=self.mat.copy())
309
310    def assign_mat(self, mat: np.ndarray):
311        with self.writable_context:
312            object.__setattr__(self, 'mat', mat)
313
314    @classmethod
315    def unpack_element_value_pairs(
316        cls,
317        is_prob: bool,
318        element_value_pairs: Iterable[Tuple[_E, Union['ScoreMap', np.ndarray, float]]],
319    ):
320        elements: List[_E] = []
321
322        values: List[Union[ScoreMap, np.ndarray, float]] = []
323        for box, value in element_value_pairs:
324            elements.append(box)
325
326            if is_prob:
327                if isinstance(value, ScoreMap):
328                    assert (0.0 <= value.mat).all() and (value.mat <= 1.0).all()
329                elif isinstance(value, np.ndarray):
330                    assert (0.0 <= value).all() and (value <= 1.0).all()
331                elif isinstance(value, float):
332                    assert 0.0 <= value <= 1.0
333                else:
334                    raise NotImplementedError()
335            values.append(value)
336
337        return elements, values
338
339    def fill_by_box_value_pairs(
340        self,
341        box_value_pairs: Iterable[Tuple['Box', Union['ScoreMap', np.ndarray, float]]],
342        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
343        keep_max_value: bool = False,
344        keep_min_value: bool = False,
345        skip_values_uniqueness_check: bool = False,
346    ):
347        boxes, values = self.unpack_element_value_pairs(self.is_prob, box_value_pairs)
348
349        boxes_mask = generate_fill_by_boxes_mask(self.shape, boxes, mode)
350        if boxes_mask is None:
351            for box, value in zip(boxes, values):
352                box.fill_score_map(
353                    score_map=self,
354                    value=value,
355                    keep_max_value=keep_max_value,
356                    keep_min_value=keep_min_value,
357                )
358
359        else:
360            unique = True
361            if not skip_values_uniqueness_check:
362                unique = check_elements_uniqueness(values)
363
364            if unique:
365                boxes_mask.fill_score_map(
366                    score_map=self,
367                    value=values[0],
368                    keep_max_value=keep_max_value,
369                    keep_min_value=keep_min_value,
370                )
371            else:
372                for box, value in zip(boxes, values):
373                    box_mask = box.extract_mask(boxes_mask).to_box_attached(box)
374                    box_mask.fill_score_map(
375                        score_map=self,
376                        value=value,
377                        keep_max_value=keep_max_value,
378                        keep_min_value=keep_min_value,
379                    )
380
381    def fill_by_boxes(
382        self,
383        boxes: Iterable['Box'],
384        value: Union['ScoreMap', np.ndarray, float] = 1.0,
385        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
386        keep_max_value: bool = False,
387        keep_min_value: bool = False,
388    ):
389        self.fill_by_box_value_pairs(
390            box_value_pairs=((box, value) for box in boxes),
391            mode=mode,
392            keep_max_value=keep_max_value,
393            keep_min_value=keep_min_value,
394            skip_values_uniqueness_check=True,
395        )
396
397    def fill_by_polygon_value_pairs(
398        self,
399        polygon_value_pairs: Iterable[Tuple['Polygon', Union['ScoreMap', np.ndarray, float]]],
400        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
401        keep_max_value: bool = False,
402        keep_min_value: bool = False,
403        skip_values_uniqueness_check: bool = False,
404    ):
405        polygons, values = self.unpack_element_value_pairs(self.is_prob, polygon_value_pairs)
406
407        polygons_mask = generate_fill_by_polygons_mask(self.shape, polygons, mode)
408        if polygons_mask is None:
409            for polygon, value in zip(polygons, values):
410                polygon.fill_score_map(
411                    score_map=self,
412                    value=value,
413                    keep_max_value=keep_max_value,
414                    keep_min_value=keep_min_value,
415                )
416
417        else:
418            unique = True
419            if not skip_values_uniqueness_check:
420                unique = check_elements_uniqueness(values)
421
422            if unique:
423                polygons_mask.fill_score_map(
424                    score_map=self,
425                    value=values[0],
426                    keep_max_value=keep_max_value,
427                    keep_min_value=keep_min_value,
428                )
429            else:
430                for polygon, value in zip(polygons, values):
431                    bounding_box = polygon.to_bounding_box()
432                    polygon_mask = bounding_box.extract_mask(polygons_mask)
433                    polygon_mask = polygon_mask.to_box_attached(bounding_box)
434                    polygon_mask.fill_score_map(
435                        score_map=self,
436                        value=value,
437                        keep_max_value=keep_max_value,
438                        keep_min_value=keep_min_value,
439                    )
440
441    def fill_by_polygons(
442        self,
443        polygons: Iterable['Polygon'],
444        value: Union['ScoreMap', np.ndarray, float] = 1.0,
445        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
446        keep_max_value: bool = False,
447        keep_min_value: bool = False,
448    ):
449        self.fill_by_polygon_value_pairs(
450            polygon_value_pairs=((polygon, value) for polygon in polygons),
451            mode=mode,
452            keep_max_value=keep_max_value,
453            keep_min_value=keep_min_value,
454            skip_values_uniqueness_check=True,
455        )
456
457    def fill_by_mask_value_pairs(
458        self,
459        mask_value_pairs: Iterable[Tuple['Mask', Union['ScoreMap', np.ndarray, float]]],
460        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
461        keep_max_value: bool = False,
462        keep_min_value: bool = False,
463        skip_values_uniqueness_check: bool = False,
464    ):
465        masks, values = self.unpack_element_value_pairs(self.is_prob, mask_value_pairs)
466
467        masks_mask = generate_fill_by_masks_mask(self.shape, masks, mode)
468        if masks_mask is None:
469            for mask, value in zip(masks, values):
470                mask.fill_score_map(
471                    score_map=self,
472                    value=value,
473                    keep_max_value=keep_max_value,
474                    keep_min_value=keep_min_value,
475                )
476
477        else:
478            unique = True
479            if not skip_values_uniqueness_check:
480                unique = check_elements_uniqueness(values)
481
482            if unique:
483                masks_mask.fill_score_map(
484                    score_map=self,
485                    value=values[0],
486                    keep_max_value=keep_max_value,
487                    keep_min_value=keep_min_value,
488                )
489            else:
490                for mask, value in zip(masks, values):
491                    if mask.box:
492                        boxed_mask = mask.box.extract_mask(masks_mask)
493                    else:
494                        boxed_mask = masks_mask
495
496                    boxed_mask = boxed_mask.copy()
497                    mask.to_inverted_mask().fill_mask(boxed_mask, value=0)
498                    boxed_mask.fill_score_map(
499                        score_map=self,
500                        value=value,
501                        keep_max_value=keep_max_value,
502                        keep_min_value=keep_min_value,
503                    )
504
505    def fill_by_masks(
506        self,
507        masks: Iterable['Mask'],
508        value: Union['ScoreMap', np.ndarray, float] = 1.0,
509        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
510        keep_max_value: bool = False,
511        keep_min_value: bool = False,
512    ):
513        self.fill_by_mask_value_pairs(
514            mask_value_pairs=((mask, value) for mask in masks),
515            mode=mode,
516            keep_max_value=keep_max_value,
517            keep_min_value=keep_min_value,
518            skip_values_uniqueness_check=True,
519        )
520
521    def __setitem__(
522        self,
523        element: Union[
524            'Box',
525            'Polygon',
526            'Mask',
527        ],
528        config: Union[
529            'ScoreMap',
530            np.ndarray,
531            float,
532            ScoreMapSetItemConfig,
533        ],
534    ):  # yapf: disable
535        if not isinstance(config, ScoreMapSetItemConfig):
536            value = config
537            keep_max_value = False
538            keep_min_value = False
539        else:
540            assert isinstance(config, ScoreMapSetItemConfig)
541            value = config.value
542            keep_max_value = config.keep_max_value
543            keep_min_value = config.keep_min_value
544
545        element.fill_score_map(
546            score_map=self,
547            value=value,
548            keep_min_value=keep_min_value,
549            keep_max_value=keep_max_value,
550        )
551
552    def __getitem__(
553        self,
554        element: Union[
555            'Box',
556            'Polygon',
557            'Mask',
558        ],
559    ):  # yapf: disable
560        return element.extract_score_map(self)
561
562    def fill_by_quad_interpolation(
563        self,
564        point0: 'Point',
565        point1: 'Point',
566        point2: 'Point',
567        point3: 'Point',
568        func_np_uv_to_mat: Callable[[np.ndarray], np.ndarray],
569        keep_max_value: bool = False,
570        keep_min_value: bool = False,
571    ):
572        score_map = self.from_quad_interpolation(
573            point0=point0,
574            point1=point1,
575            point2=point2,
576            point3=point3,
577            func_np_uv_to_mat=func_np_uv_to_mat,
578            is_prob=self.is_prob,
579        )
580        assert score_map.box
581        with self.writable_context:
582            score_map.box.fill_np_array(
583                mat=self.mat,
584                value=score_map.mat,
585                np_mask=(score_map.mat > 0.0),
586                keep_max_value=keep_max_value,
587                keep_min_value=keep_min_value,
588            )
589
590    def to_shifted_score_map(self, offset_y: int = 0, offset_x: int = 0):
591        assert self.box
592        shifted_box = self.box.to_shifted_box(offset_y=offset_y, offset_x=offset_x)
593        return attrs.evolve(self, box=shifted_box)
594
595    def to_conducted_resized_polygon(
596        self,
597        shapable_or_shape: Union[Shapable, Tuple[int, int]],
598        resized_height: Optional[int] = None,
599        resized_width: Optional[int] = None,
600        cv_resize_interpolation: int = cv.INTER_CUBIC,
601    ):
602        assert self.box
603        resized_box = self.box.to_conducted_resized_box(
604            shapable_or_shape=shapable_or_shape,
605            resized_height=resized_height,
606            resized_width=resized_width,
607        )
608        resized_score_map = self.to_box_detached().to_resized_score_map(
609            resized_height=resized_box.height,
610            resized_width=resized_box.width,
611            cv_resize_interpolation=cv_resize_interpolation,
612        )
613        resized_score_map = resized_score_map.to_box_attached(resized_box)
614        return resized_score_map
615
616    def to_resized_score_map(
617        self,
618        resized_height: Optional[int] = None,
619        resized_width: Optional[int] = None,
620        cv_resize_interpolation: int = cv.INTER_CUBIC,
621    ):
622        assert not self.box
623        _, _, resized_height, resized_width = generate_shape_and_resized_shape(
624            shapable_or_shape=self.shape,
625            resized_height=resized_height,
626            resized_width=resized_width,
627        )
628        mat = cv.resize(
629            self.mat,
630            (resized_width, resized_height),
631            interpolation=cv_resize_interpolation,
632        )
633        if self.is_prob:
634            # NOTE: Interpolation like bi-cubic could generate out-of-bound values.
635            mat = np.clip(mat, 0.0, 1.0)
636        return attrs.evolve(self, mat=mat)
637
638    def to_cropped_score_map(
639        self,
640        up: Optional[int] = None,
641        down: Optional[int] = None,
642        left: Optional[int] = None,
643        right: Optional[int] = None,
644    ):
645        assert not self.box
646
647        up = up or 0
648        down = down or self.height - 1
649        left = left or 0
650        right = right or self.width - 1
651
652        return attrs.evolve(self, mat=self.mat[up:down + 1, left:right + 1])
653
654    def to_box_attached(self, box: 'Box'):
655        assert self.height == box.height
656        assert self.width == box.width
657        return attrs.evolve(self, box=box)
658
659    def to_box_detached(self):
660        assert self.box
661        return attrs.evolve(self, box=None)
662
663    def fill_np_array(
664        self,
665        mat: np.ndarray,
666        value: Union[np.ndarray, Tuple[float, ...], float],
667        keep_max_value: bool = False,
668        keep_min_value: bool = False,
669    ):
670        self.equivalent_box.fill_np_array(
671            mat=mat,
672            value=value,
673            alpha=self,
674            keep_max_value=keep_max_value,
675            keep_min_value=keep_min_value,
676        )
677
678    def fill_image(
679        self,
680        image: 'Image',
681        value: Union['Image', np.ndarray, Tuple[int, ...], int],
682    ):
683        self.equivalent_box.fill_image(
684            image=image,
685            value=value,
686            alpha=self,
687        )
688
689    def to_mask(self, threshold: float = 0.0):
690        mat = (self.mat > threshold).astype(np.uint8)
691        return Mask(mat=mat, box=self.box)
692
693
694def generate_fill_by_score_maps_mask(
695    shape: Tuple[int, int],
696    score_maps: Iterable[ScoreMap],
697    mode: ElementSetOperationMode,
698):
699    if mode == ElementSetOperationMode.UNION:
700        return None
701    else:
702        return Mask.from_score_maps(shape, score_maps, mode)
703
704
705# Cyclic dependency, by design.
706from .uniqueness import check_elements_uniqueness  # noqa: E402
707from .image import Image  # noqa: E402
708from .point import Point  # noqa: E402
709from .box import Box, generate_fill_by_boxes_mask  # noqa: E402
710from .mask import Mask, generate_fill_by_masks_mask  # noqa: E402
711from .polygon import Polygon, generate_fill_by_polygons_mask  # noqa: E402
class ScoreMapSetItemConfig:
27class ScoreMapSetItemConfig:
28    value: Union[
29        'ScoreMap',
30        np.ndarray,
31        float,
32    ] = 1.0  # yapf: disable
33    keep_max_value: bool = False
34    keep_min_value: bool = False
ScoreMapSetItemConfig( value: Union[vkit.element.score_map.ScoreMap, numpy.ndarray, float] = 1.0, keep_max_value: bool = False, keep_min_value: bool = False)
2def __init__(self, value=attr_dict['value'].default, keep_max_value=attr_dict['keep_max_value'].default, keep_min_value=attr_dict['keep_min_value'].default):
3    self.value = value
4    self.keep_max_value = keep_max_value
5    self.keep_min_value = keep_min_value

Method generated by attrs for class ScoreMapSetItemConfig.

class NpVec:
38class NpVec:
39    x: np.ndarray
40    y: np.ndarray
41
42    @classmethod
43    def from_point(cls, point: 'Point'):
44        return cls(
45            x=np.asarray(point.smooth_x, dtype=np.float32),
46            y=np.asarray(point.smooth_y, dtype=np.float32),
47        )
48
49    def __add__(self, other: 'NpVec'):
50        return NpVec(x=self.x + other.x, y=self.y + other.y)
51
52    def __sub__(self, other: 'NpVec'):
53        return NpVec(x=self.x - other.x, y=self.y - other.y)  # type: ignore
54
55    def __mul__(self, other: 'NpVec') -> np.ndarray:
56        return self.x * other.y - self.y * other.x  # type: ignore
NpVec(x: numpy.ndarray, y: numpy.ndarray)
2def __init__(self, x, y):
3    self.x = x
4    self.y = y

Method generated by attrs for class NpVec.

@classmethod
def from_point(cls, point: vkit.element.point.Point):
42    @classmethod
43    def from_point(cls, point: 'Point'):
44        return cls(
45            x=np.asarray(point.smooth_x, dtype=np.float32),
46            y=np.asarray(point.smooth_y, dtype=np.float32),
47        )
class WritableScoreMapContextDecorator(contextlib.ContextDecorator):
59class WritableScoreMapContextDecorator(ContextDecorator):
60
61    def __init__(self, score_map: 'ScoreMap'):
62        super().__init__()
63        self.score_map = score_map
64
65    def __enter__(self):
66        if self.score_map.mat.flags.c_contiguous:
67            assert not self.score_map.mat.flags.writeable
68
69        try:
70            self.score_map.mat.flags.writeable = True
71        except ValueError:
72            # Copy on write.
73            object.__setattr__(
74                self.score_map,
75                'mat',
76                np.array(self.score_map.mat),
77            )
78            assert self.score_map.mat.flags.writeable
79
80    def __exit__(self, *exc):  # type: ignore
81        self.score_map.mat.flags.writeable = False

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

WritableScoreMapContextDecorator(score_map: vkit.element.score_map.ScoreMap)
61    def __init__(self, score_map: 'ScoreMap'):
62        super().__init__()
63        self.score_map = score_map
class ScoreMap(vkit.element.type.Shapable):
 88class ScoreMap(Shapable):
 89    mat: np.ndarray
 90    box: Optional['Box'] = None
 91    is_prob: bool = True
 92
 93    def __attrs_post_init__(self):
 94        if self.mat.ndim != 2:
 95            raise RuntimeError('ndim should == 2.')
 96        if self.box and self.shape != self.box.shape:
 97            raise RuntimeError('self.shape != box.shape.')
 98
 99        if self.mat.dtype != np.float32:
100            raise RuntimeError('mat.dtype != np.float32')
101
102        # For the control of write.
103        self.mat.flags.writeable = False
104
105        if self.is_prob:
106            score_min = self.mat.min()
107            score_max = self.mat.max()
108            if score_min < 0.0 or score_max > 1.0:
109                raise RuntimeError('score not in range [0.0, 1.0]')
110
111    ###############
112    # Constructor #
113    ###############
114    @classmethod
115    def from_shape(
116        cls,
117        shape: Tuple[int, int],
118        value: float = 0.0,
119        is_prob: bool = True,
120    ):
121        height, width = shape
122        if is_prob:
123            assert 0.0 <= value <= 1.0
124        mat = np.full((height, width), fill_value=value, dtype=np.float32)
125        return cls(mat=mat, is_prob=is_prob)
126
127    @classmethod
128    def from_shapable(
129        cls,
130        shapable: Shapable,
131        value: float = 0.0,
132        is_prob: bool = True,
133    ):
134        return cls.from_shape(
135            shape=shapable.shape,
136            value=value,
137            is_prob=is_prob,
138        )
139
140    @classmethod
141    def from_quad_interpolation(
142        cls,
143        point0: 'Point',
144        point1: 'Point',
145        point2: 'Point',
146        point3: 'Point',
147        func_np_uv_to_mat: Callable[[np.ndarray], np.ndarray],
148        is_prob: bool = True,
149    ):
150        '''
151        Ref: https://www.reedbeta.com/blog/quadrilateral-interpolation-part-2/
152
153        points in clockwise order:
154        point0 0.0 -- u --> 1.0 point1
155                                 0.0
156                                  ↓
157                                  v
158                                  ↓
159                                 1.0
160        point3 0.0 <-- u -- 1.0 point2
161
162        lerp(a, b, r) = a + r * (b - a)
163        pointx(u, v) = lerp(
164            lerp(point0, point1, u),
165            lerp(point3, point2, u),
166            v,
167        )
168
169        Hence,
170        u in [0.0, 1.0], and
171        u -> 0.0, x -> line (point0, point3)
172        u -> 1.0, x -> line (point1, point2)
173
174        v in [0.0, 1.0], and
175        v -> 0.0, x -> line (point0, point1)
176        v -> 1.0, x -> line (point3, point2)
177        '''
178        polygon = Polygon.create((
179            point0,
180            point1,
181            point2,
182            point3,
183        ))
184        bounding_box = polygon.bounding_box
185        self_relative_polygon = polygon.self_relative_polygon
186        np_active_mask = polygon.internals.np_mask
187
188        np_vec_0 = NpVec.from_point(self_relative_polygon.points[0])
189        np_vec_1 = NpVec.from_point(self_relative_polygon.points[1])
190        np_vec_2 = NpVec.from_point(self_relative_polygon.points[2])
191        np_vec_3 = NpVec.from_point(self_relative_polygon.points[3])
192
193        # F(x, y) -> x
194        np_pointx_x = np.repeat(
195            np.expand_dims(
196                np.arange(bounding_box.width, dtype=np.int32),
197                axis=0,
198            ),
199            bounding_box.height,
200            axis=0,
201        )
202        # F(x, y) -> y
203        np_pointx_y = np.repeat(
204            np.expand_dims(
205                np.arange(bounding_box.height, dtype=np.int32),
206                axis=1,
207            ),
208            bounding_box.width,
209            axis=1,
210        )
211        np_vec_x = NpVec(x=np_pointx_x, y=np_pointx_y)
212
213        np_vec_q = np_vec_x - np_vec_0
214        np_vec_b1 = np_vec_1 - np_vec_0
215        np_vec_b2 = np_vec_3 - np_vec_0
216        np_vec_b3 = ((np_vec_0 - np_vec_1) - np_vec_3) + np_vec_2
217
218        scale_a = float(np_vec_b2 * np_vec_b3)
219
220        np_b: np.ndarray = (np_vec_b3 * np_vec_q) - (np_vec_b1 * np_vec_b2)  # type: ignore
221        np_b = np_b.astype(np.float32)
222
223        np_c = np_vec_b1 * np_vec_q
224        np_c = np_c.astype(np.float32)
225
226        # Solve v.
227        if abs(scale_a) < 0.001:
228            np_v = -np_c / np_b
229
230        else:
231            np_discrim = np.sqrt(np.power(np_b, 2) - 4 * scale_a * np_c)
232            scale_i2a = 0.5 / scale_a
233            np_v_pos = (-np_b + np_discrim) * scale_i2a
234            np_v_neg: np.ndarray = (-np_b - np_discrim) * scale_i2a  # type: ignore
235
236            np_masked_v_pos: np.ndarray = np_v_pos[np_active_mask]
237            np_v_pos_valid = (0.0 <= np_masked_v_pos) & (np_masked_v_pos <= 1.0)
238
239            np_masked_v_neg: np.ndarray = np_v_neg[np_active_mask]
240            np_v_neg_valid = (0.0 <= np_masked_v_neg) & (np_masked_v_neg <= 1.0)
241
242            if np_v_pos_valid.sum() >= np_v_neg_valid.sum():
243                np_v = np_v_pos
244            else:
245                np_v = np_v_neg
246
247        np_v[~np_active_mask] = 0.0
248        np_v = np.clip(np_v, 0.0, 1.0)
249
250        # Solve u.
251        np_u = np.zeros_like(np_v)
252
253        np_denom_x: np.ndarray = np_vec_b1.x + np_vec_b3.x * np_v
254        np_denom_y: np.ndarray = np_vec_b1.y + np_vec_b3.y * np_v
255
256        np_denom_x_mask = (np.abs(np_denom_x) > np.abs(np_denom_y)) & (np_denom_x != 0.0)
257        if np_denom_x_mask.any():
258            np_q_x = np_vec_q.x
259            np_u[np_denom_x_mask] = (
260                (np_q_x[np_denom_x_mask] - np_vec_b2.x * np_v[np_denom_x_mask])
261                / np_denom_x[np_denom_x_mask]
262            )
263
264        np_denom_y_mask = (~np_denom_x_mask) & (np_denom_y != 0.0)
265        if np_denom_y_mask.any():
266            np_q_y = np_vec_q.y
267            np_u[np_denom_y_mask] = (
268                (np_q_y[np_denom_y_mask] - np_vec_b2.y * np_v[np_denom_y_mask])
269                / np_denom_y[np_denom_y_mask]
270            )
271
272        np_u[~np_active_mask] = 0.0
273        np_u = np.clip(np_u, 0.0, 1.0)
274
275        # Stack to (height, width, 2)
276        np_uv = np.stack((np_u, np_v), axis=-1)
277
278        # Mat.
279        mat = func_np_uv_to_mat(np_uv)
280        return cls(
281            mat=mat,
282            box=bounding_box,
283            is_prob=is_prob,
284        )
285
286    ############
287    # Property #
288    ############
289    @property
290    def height(self):
291        return self.mat.shape[0]
292
293    @property
294    def width(self):
295        return self.mat.shape[1]
296
297    @property
298    def equivalent_box(self):
299        return self.box or Box.from_shapable(self)
300
301    @property
302    def writable_context(self):
303        return WritableScoreMapContextDecorator(self)
304
305    ############
306    # Operator #
307    ############
308    def copy(self):
309        return attrs.evolve(self, mat=self.mat.copy())
310
311    def assign_mat(self, mat: np.ndarray):
312        with self.writable_context:
313            object.__setattr__(self, 'mat', mat)
314
315    @classmethod
316    def unpack_element_value_pairs(
317        cls,
318        is_prob: bool,
319        element_value_pairs: Iterable[Tuple[_E, Union['ScoreMap', np.ndarray, float]]],
320    ):
321        elements: List[_E] = []
322
323        values: List[Union[ScoreMap, np.ndarray, float]] = []
324        for box, value in element_value_pairs:
325            elements.append(box)
326
327            if is_prob:
328                if isinstance(value, ScoreMap):
329                    assert (0.0 <= value.mat).all() and (value.mat <= 1.0).all()
330                elif isinstance(value, np.ndarray):
331                    assert (0.0 <= value).all() and (value <= 1.0).all()
332                elif isinstance(value, float):
333                    assert 0.0 <= value <= 1.0
334                else:
335                    raise NotImplementedError()
336            values.append(value)
337
338        return elements, values
339
340    def fill_by_box_value_pairs(
341        self,
342        box_value_pairs: Iterable[Tuple['Box', Union['ScoreMap', np.ndarray, float]]],
343        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
344        keep_max_value: bool = False,
345        keep_min_value: bool = False,
346        skip_values_uniqueness_check: bool = False,
347    ):
348        boxes, values = self.unpack_element_value_pairs(self.is_prob, box_value_pairs)
349
350        boxes_mask = generate_fill_by_boxes_mask(self.shape, boxes, mode)
351        if boxes_mask is None:
352            for box, value in zip(boxes, values):
353                box.fill_score_map(
354                    score_map=self,
355                    value=value,
356                    keep_max_value=keep_max_value,
357                    keep_min_value=keep_min_value,
358                )
359
360        else:
361            unique = True
362            if not skip_values_uniqueness_check:
363                unique = check_elements_uniqueness(values)
364
365            if unique:
366                boxes_mask.fill_score_map(
367                    score_map=self,
368                    value=values[0],
369                    keep_max_value=keep_max_value,
370                    keep_min_value=keep_min_value,
371                )
372            else:
373                for box, value in zip(boxes, values):
374                    box_mask = box.extract_mask(boxes_mask).to_box_attached(box)
375                    box_mask.fill_score_map(
376                        score_map=self,
377                        value=value,
378                        keep_max_value=keep_max_value,
379                        keep_min_value=keep_min_value,
380                    )
381
382    def fill_by_boxes(
383        self,
384        boxes: Iterable['Box'],
385        value: Union['ScoreMap', np.ndarray, float] = 1.0,
386        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
387        keep_max_value: bool = False,
388        keep_min_value: bool = False,
389    ):
390        self.fill_by_box_value_pairs(
391            box_value_pairs=((box, value) for box in boxes),
392            mode=mode,
393            keep_max_value=keep_max_value,
394            keep_min_value=keep_min_value,
395            skip_values_uniqueness_check=True,
396        )
397
398    def fill_by_polygon_value_pairs(
399        self,
400        polygon_value_pairs: Iterable[Tuple['Polygon', Union['ScoreMap', np.ndarray, float]]],
401        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
402        keep_max_value: bool = False,
403        keep_min_value: bool = False,
404        skip_values_uniqueness_check: bool = False,
405    ):
406        polygons, values = self.unpack_element_value_pairs(self.is_prob, polygon_value_pairs)
407
408        polygons_mask = generate_fill_by_polygons_mask(self.shape, polygons, mode)
409        if polygons_mask is None:
410            for polygon, value in zip(polygons, values):
411                polygon.fill_score_map(
412                    score_map=self,
413                    value=value,
414                    keep_max_value=keep_max_value,
415                    keep_min_value=keep_min_value,
416                )
417
418        else:
419            unique = True
420            if not skip_values_uniqueness_check:
421                unique = check_elements_uniqueness(values)
422
423            if unique:
424                polygons_mask.fill_score_map(
425                    score_map=self,
426                    value=values[0],
427                    keep_max_value=keep_max_value,
428                    keep_min_value=keep_min_value,
429                )
430            else:
431                for polygon, value in zip(polygons, values):
432                    bounding_box = polygon.to_bounding_box()
433                    polygon_mask = bounding_box.extract_mask(polygons_mask)
434                    polygon_mask = polygon_mask.to_box_attached(bounding_box)
435                    polygon_mask.fill_score_map(
436                        score_map=self,
437                        value=value,
438                        keep_max_value=keep_max_value,
439                        keep_min_value=keep_min_value,
440                    )
441
442    def fill_by_polygons(
443        self,
444        polygons: Iterable['Polygon'],
445        value: Union['ScoreMap', np.ndarray, float] = 1.0,
446        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
447        keep_max_value: bool = False,
448        keep_min_value: bool = False,
449    ):
450        self.fill_by_polygon_value_pairs(
451            polygon_value_pairs=((polygon, value) for polygon in polygons),
452            mode=mode,
453            keep_max_value=keep_max_value,
454            keep_min_value=keep_min_value,
455            skip_values_uniqueness_check=True,
456        )
457
458    def fill_by_mask_value_pairs(
459        self,
460        mask_value_pairs: Iterable[Tuple['Mask', Union['ScoreMap', np.ndarray, float]]],
461        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
462        keep_max_value: bool = False,
463        keep_min_value: bool = False,
464        skip_values_uniqueness_check: bool = False,
465    ):
466        masks, values = self.unpack_element_value_pairs(self.is_prob, mask_value_pairs)
467
468        masks_mask = generate_fill_by_masks_mask(self.shape, masks, mode)
469        if masks_mask is None:
470            for mask, value in zip(masks, values):
471                mask.fill_score_map(
472                    score_map=self,
473                    value=value,
474                    keep_max_value=keep_max_value,
475                    keep_min_value=keep_min_value,
476                )
477
478        else:
479            unique = True
480            if not skip_values_uniqueness_check:
481                unique = check_elements_uniqueness(values)
482
483            if unique:
484                masks_mask.fill_score_map(
485                    score_map=self,
486                    value=values[0],
487                    keep_max_value=keep_max_value,
488                    keep_min_value=keep_min_value,
489                )
490            else:
491                for mask, value in zip(masks, values):
492                    if mask.box:
493                        boxed_mask = mask.box.extract_mask(masks_mask)
494                    else:
495                        boxed_mask = masks_mask
496
497                    boxed_mask = boxed_mask.copy()
498                    mask.to_inverted_mask().fill_mask(boxed_mask, value=0)
499                    boxed_mask.fill_score_map(
500                        score_map=self,
501                        value=value,
502                        keep_max_value=keep_max_value,
503                        keep_min_value=keep_min_value,
504                    )
505
506    def fill_by_masks(
507        self,
508        masks: Iterable['Mask'],
509        value: Union['ScoreMap', np.ndarray, float] = 1.0,
510        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
511        keep_max_value: bool = False,
512        keep_min_value: bool = False,
513    ):
514        self.fill_by_mask_value_pairs(
515            mask_value_pairs=((mask, value) for mask in masks),
516            mode=mode,
517            keep_max_value=keep_max_value,
518            keep_min_value=keep_min_value,
519            skip_values_uniqueness_check=True,
520        )
521
522    def __setitem__(
523        self,
524        element: Union[
525            'Box',
526            'Polygon',
527            'Mask',
528        ],
529        config: Union[
530            'ScoreMap',
531            np.ndarray,
532            float,
533            ScoreMapSetItemConfig,
534        ],
535    ):  # yapf: disable
536        if not isinstance(config, ScoreMapSetItemConfig):
537            value = config
538            keep_max_value = False
539            keep_min_value = False
540        else:
541            assert isinstance(config, ScoreMapSetItemConfig)
542            value = config.value
543            keep_max_value = config.keep_max_value
544            keep_min_value = config.keep_min_value
545
546        element.fill_score_map(
547            score_map=self,
548            value=value,
549            keep_min_value=keep_min_value,
550            keep_max_value=keep_max_value,
551        )
552
553    def __getitem__(
554        self,
555        element: Union[
556            'Box',
557            'Polygon',
558            'Mask',
559        ],
560    ):  # yapf: disable
561        return element.extract_score_map(self)
562
563    def fill_by_quad_interpolation(
564        self,
565        point0: 'Point',
566        point1: 'Point',
567        point2: 'Point',
568        point3: 'Point',
569        func_np_uv_to_mat: Callable[[np.ndarray], np.ndarray],
570        keep_max_value: bool = False,
571        keep_min_value: bool = False,
572    ):
573        score_map = self.from_quad_interpolation(
574            point0=point0,
575            point1=point1,
576            point2=point2,
577            point3=point3,
578            func_np_uv_to_mat=func_np_uv_to_mat,
579            is_prob=self.is_prob,
580        )
581        assert score_map.box
582        with self.writable_context:
583            score_map.box.fill_np_array(
584                mat=self.mat,
585                value=score_map.mat,
586                np_mask=(score_map.mat > 0.0),
587                keep_max_value=keep_max_value,
588                keep_min_value=keep_min_value,
589            )
590
591    def to_shifted_score_map(self, offset_y: int = 0, offset_x: int = 0):
592        assert self.box
593        shifted_box = self.box.to_shifted_box(offset_y=offset_y, offset_x=offset_x)
594        return attrs.evolve(self, box=shifted_box)
595
596    def to_conducted_resized_polygon(
597        self,
598        shapable_or_shape: Union[Shapable, Tuple[int, int]],
599        resized_height: Optional[int] = None,
600        resized_width: Optional[int] = None,
601        cv_resize_interpolation: int = cv.INTER_CUBIC,
602    ):
603        assert self.box
604        resized_box = self.box.to_conducted_resized_box(
605            shapable_or_shape=shapable_or_shape,
606            resized_height=resized_height,
607            resized_width=resized_width,
608        )
609        resized_score_map = self.to_box_detached().to_resized_score_map(
610            resized_height=resized_box.height,
611            resized_width=resized_box.width,
612            cv_resize_interpolation=cv_resize_interpolation,
613        )
614        resized_score_map = resized_score_map.to_box_attached(resized_box)
615        return resized_score_map
616
617    def to_resized_score_map(
618        self,
619        resized_height: Optional[int] = None,
620        resized_width: Optional[int] = None,
621        cv_resize_interpolation: int = cv.INTER_CUBIC,
622    ):
623        assert not self.box
624        _, _, resized_height, resized_width = generate_shape_and_resized_shape(
625            shapable_or_shape=self.shape,
626            resized_height=resized_height,
627            resized_width=resized_width,
628        )
629        mat = cv.resize(
630            self.mat,
631            (resized_width, resized_height),
632            interpolation=cv_resize_interpolation,
633        )
634        if self.is_prob:
635            # NOTE: Interpolation like bi-cubic could generate out-of-bound values.
636            mat = np.clip(mat, 0.0, 1.0)
637        return attrs.evolve(self, mat=mat)
638
639    def to_cropped_score_map(
640        self,
641        up: Optional[int] = None,
642        down: Optional[int] = None,
643        left: Optional[int] = None,
644        right: Optional[int] = None,
645    ):
646        assert not self.box
647
648        up = up or 0
649        down = down or self.height - 1
650        left = left or 0
651        right = right or self.width - 1
652
653        return attrs.evolve(self, mat=self.mat[up:down + 1, left:right + 1])
654
655    def to_box_attached(self, box: 'Box'):
656        assert self.height == box.height
657        assert self.width == box.width
658        return attrs.evolve(self, box=box)
659
660    def to_box_detached(self):
661        assert self.box
662        return attrs.evolve(self, box=None)
663
664    def fill_np_array(
665        self,
666        mat: np.ndarray,
667        value: Union[np.ndarray, Tuple[float, ...], float],
668        keep_max_value: bool = False,
669        keep_min_value: bool = False,
670    ):
671        self.equivalent_box.fill_np_array(
672            mat=mat,
673            value=value,
674            alpha=self,
675            keep_max_value=keep_max_value,
676            keep_min_value=keep_min_value,
677        )
678
679    def fill_image(
680        self,
681        image: 'Image',
682        value: Union['Image', np.ndarray, Tuple[int, ...], int],
683    ):
684        self.equivalent_box.fill_image(
685            image=image,
686            value=value,
687            alpha=self,
688        )
689
690    def to_mask(self, threshold: float = 0.0):
691        mat = (self.mat > threshold).astype(np.uint8)
692        return Mask(mat=mat, box=self.box)
ScoreMap( mat: numpy.ndarray, box: Union[vkit.element.box.Box, NoneType] = None, is_prob: bool = True)
2def __init__(self, mat, box=attr_dict['box'].default, is_prob=attr_dict['is_prob'].default):
3    _setattr(self, 'mat', mat)
4    _setattr(self, 'box', box)
5    _setattr(self, 'is_prob', is_prob)
6    self.__attrs_post_init__()

Method generated by attrs for class ScoreMap.

@classmethod
def from_shape( cls, shape: Tuple[int, int], value: float = 0.0, is_prob: bool = True):
114    @classmethod
115    def from_shape(
116        cls,
117        shape: Tuple[int, int],
118        value: float = 0.0,
119        is_prob: bool = True,
120    ):
121        height, width = shape
122        if is_prob:
123            assert 0.0 <= value <= 1.0
124        mat = np.full((height, width), fill_value=value, dtype=np.float32)
125        return cls(mat=mat, is_prob=is_prob)
@classmethod
def from_shapable( cls, shapable: vkit.element.type.Shapable, value: float = 0.0, is_prob: bool = True):
127    @classmethod
128    def from_shapable(
129        cls,
130        shapable: Shapable,
131        value: float = 0.0,
132        is_prob: bool = True,
133    ):
134        return cls.from_shape(
135            shape=shapable.shape,
136            value=value,
137            is_prob=is_prob,
138        )
@classmethod
def from_quad_interpolation( cls, point0: vkit.element.point.Point, point1: vkit.element.point.Point, point2: vkit.element.point.Point, point3: vkit.element.point.Point, func_np_uv_to_mat: Callable[[numpy.ndarray], numpy.ndarray], is_prob: bool = True):
140    @classmethod
141    def from_quad_interpolation(
142        cls,
143        point0: 'Point',
144        point1: 'Point',
145        point2: 'Point',
146        point3: 'Point',
147        func_np_uv_to_mat: Callable[[np.ndarray], np.ndarray],
148        is_prob: bool = True,
149    ):
150        '''
151        Ref: https://www.reedbeta.com/blog/quadrilateral-interpolation-part-2/
152
153        points in clockwise order:
154        point0 0.0 -- u --> 1.0 point1
155                                 0.0
156                                  ↓
157                                  v
158                                  ↓
159                                 1.0
160        point3 0.0 <-- u -- 1.0 point2
161
162        lerp(a, b, r) = a + r * (b - a)
163        pointx(u, v) = lerp(
164            lerp(point0, point1, u),
165            lerp(point3, point2, u),
166            v,
167        )
168
169        Hence,
170        u in [0.0, 1.0], and
171        u -> 0.0, x -> line (point0, point3)
172        u -> 1.0, x -> line (point1, point2)
173
174        v in [0.0, 1.0], and
175        v -> 0.0, x -> line (point0, point1)
176        v -> 1.0, x -> line (point3, point2)
177        '''
178        polygon = Polygon.create((
179            point0,
180            point1,
181            point2,
182            point3,
183        ))
184        bounding_box = polygon.bounding_box
185        self_relative_polygon = polygon.self_relative_polygon
186        np_active_mask = polygon.internals.np_mask
187
188        np_vec_0 = NpVec.from_point(self_relative_polygon.points[0])
189        np_vec_1 = NpVec.from_point(self_relative_polygon.points[1])
190        np_vec_2 = NpVec.from_point(self_relative_polygon.points[2])
191        np_vec_3 = NpVec.from_point(self_relative_polygon.points[3])
192
193        # F(x, y) -> x
194        np_pointx_x = np.repeat(
195            np.expand_dims(
196                np.arange(bounding_box.width, dtype=np.int32),
197                axis=0,
198            ),
199            bounding_box.height,
200            axis=0,
201        )
202        # F(x, y) -> y
203        np_pointx_y = np.repeat(
204            np.expand_dims(
205                np.arange(bounding_box.height, dtype=np.int32),
206                axis=1,
207            ),
208            bounding_box.width,
209            axis=1,
210        )
211        np_vec_x = NpVec(x=np_pointx_x, y=np_pointx_y)
212
213        np_vec_q = np_vec_x - np_vec_0
214        np_vec_b1 = np_vec_1 - np_vec_0
215        np_vec_b2 = np_vec_3 - np_vec_0
216        np_vec_b3 = ((np_vec_0 - np_vec_1) - np_vec_3) + np_vec_2
217
218        scale_a = float(np_vec_b2 * np_vec_b3)
219
220        np_b: np.ndarray = (np_vec_b3 * np_vec_q) - (np_vec_b1 * np_vec_b2)  # type: ignore
221        np_b = np_b.astype(np.float32)
222
223        np_c = np_vec_b1 * np_vec_q
224        np_c = np_c.astype(np.float32)
225
226        # Solve v.
227        if abs(scale_a) < 0.001:
228            np_v = -np_c / np_b
229
230        else:
231            np_discrim = np.sqrt(np.power(np_b, 2) - 4 * scale_a * np_c)
232            scale_i2a = 0.5 / scale_a
233            np_v_pos = (-np_b + np_discrim) * scale_i2a
234            np_v_neg: np.ndarray = (-np_b - np_discrim) * scale_i2a  # type: ignore
235
236            np_masked_v_pos: np.ndarray = np_v_pos[np_active_mask]
237            np_v_pos_valid = (0.0 <= np_masked_v_pos) & (np_masked_v_pos <= 1.0)
238
239            np_masked_v_neg: np.ndarray = np_v_neg[np_active_mask]
240            np_v_neg_valid = (0.0 <= np_masked_v_neg) & (np_masked_v_neg <= 1.0)
241
242            if np_v_pos_valid.sum() >= np_v_neg_valid.sum():
243                np_v = np_v_pos
244            else:
245                np_v = np_v_neg
246
247        np_v[~np_active_mask] = 0.0
248        np_v = np.clip(np_v, 0.0, 1.0)
249
250        # Solve u.
251        np_u = np.zeros_like(np_v)
252
253        np_denom_x: np.ndarray = np_vec_b1.x + np_vec_b3.x * np_v
254        np_denom_y: np.ndarray = np_vec_b1.y + np_vec_b3.y * np_v
255
256        np_denom_x_mask = (np.abs(np_denom_x) > np.abs(np_denom_y)) & (np_denom_x != 0.0)
257        if np_denom_x_mask.any():
258            np_q_x = np_vec_q.x
259            np_u[np_denom_x_mask] = (
260                (np_q_x[np_denom_x_mask] - np_vec_b2.x * np_v[np_denom_x_mask])
261                / np_denom_x[np_denom_x_mask]
262            )
263
264        np_denom_y_mask = (~np_denom_x_mask) & (np_denom_y != 0.0)
265        if np_denom_y_mask.any():
266            np_q_y = np_vec_q.y
267            np_u[np_denom_y_mask] = (
268                (np_q_y[np_denom_y_mask] - np_vec_b2.y * np_v[np_denom_y_mask])
269                / np_denom_y[np_denom_y_mask]
270            )
271
272        np_u[~np_active_mask] = 0.0
273        np_u = np.clip(np_u, 0.0, 1.0)
274
275        # Stack to (height, width, 2)
276        np_uv = np.stack((np_u, np_v), axis=-1)
277
278        # Mat.
279        mat = func_np_uv_to_mat(np_uv)
280        return cls(
281            mat=mat,
282            box=bounding_box,
283            is_prob=is_prob,
284        )

Ref: https://www.reedbeta.com/blog/quadrilateral-interpolation-part-2/

points in clockwise order: point0 0.0 -- u --> 1.0 point1 0.0 ↓ v ↓ 1.0 point3 0.0 <-- u -- 1.0 point2

lerp(a, b, r) = a + r * (b - a) pointx(u, v) = lerp( lerp(point0, point1, u), lerp(point3, point2, u), v, )

Hence, u in [0.0, 1.0], and u -> 0.0, x -> line (point0, point3) u -> 1.0, x -> line (point1, point2)

v in [0.0, 1.0], and v -> 0.0, x -> line (point0, point1) v -> 1.0, x -> line (point3, point2)

def copy(self):
308    def copy(self):
309        return attrs.evolve(self, mat=self.mat.copy())
def assign_mat(self, mat: numpy.ndarray):
311    def assign_mat(self, mat: np.ndarray):
312        with self.writable_context:
313            object.__setattr__(self, 'mat', mat)
@classmethod
def unpack_element_value_pairs( cls, is_prob: bool, element_value_pairs: collections.abc.Iterable[tuple[~_E, typing.Union[vkit.element.score_map.ScoreMap, numpy.ndarray, float]]]):
315    @classmethod
316    def unpack_element_value_pairs(
317        cls,
318        is_prob: bool,
319        element_value_pairs: Iterable[Tuple[_E, Union['ScoreMap', np.ndarray, float]]],
320    ):
321        elements: List[_E] = []
322
323        values: List[Union[ScoreMap, np.ndarray, float]] = []
324        for box, value in element_value_pairs:
325            elements.append(box)
326
327            if is_prob:
328                if isinstance(value, ScoreMap):
329                    assert (0.0 <= value.mat).all() and (value.mat <= 1.0).all()
330                elif isinstance(value, np.ndarray):
331                    assert (0.0 <= value).all() and (value <= 1.0).all()
332                elif isinstance(value, float):
333                    assert 0.0 <= value <= 1.0
334                else:
335                    raise NotImplementedError()
336            values.append(value)
337
338        return elements, values
def fill_by_box_value_pairs( self, box_value_pairs: collections.abc.Iterable[tuple[vkit.element.box.Box, typing.Union[vkit.element.score_map.ScoreMap, numpy.ndarray, float]]], mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>, keep_max_value: bool = False, keep_min_value: bool = False, skip_values_uniqueness_check: bool = False):
340    def fill_by_box_value_pairs(
341        self,
342        box_value_pairs: Iterable[Tuple['Box', Union['ScoreMap', np.ndarray, float]]],
343        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
344        keep_max_value: bool = False,
345        keep_min_value: bool = False,
346        skip_values_uniqueness_check: bool = False,
347    ):
348        boxes, values = self.unpack_element_value_pairs(self.is_prob, box_value_pairs)
349
350        boxes_mask = generate_fill_by_boxes_mask(self.shape, boxes, mode)
351        if boxes_mask is None:
352            for box, value in zip(boxes, values):
353                box.fill_score_map(
354                    score_map=self,
355                    value=value,
356                    keep_max_value=keep_max_value,
357                    keep_min_value=keep_min_value,
358                )
359
360        else:
361            unique = True
362            if not skip_values_uniqueness_check:
363                unique = check_elements_uniqueness(values)
364
365            if unique:
366                boxes_mask.fill_score_map(
367                    score_map=self,
368                    value=values[0],
369                    keep_max_value=keep_max_value,
370                    keep_min_value=keep_min_value,
371                )
372            else:
373                for box, value in zip(boxes, values):
374                    box_mask = box.extract_mask(boxes_mask).to_box_attached(box)
375                    box_mask.fill_score_map(
376                        score_map=self,
377                        value=value,
378                        keep_max_value=keep_max_value,
379                        keep_min_value=keep_min_value,
380                    )
def fill_by_boxes( self, boxes: collections.abc.Iterable[vkit.element.box.Box], value: Union[vkit.element.score_map.ScoreMap, numpy.ndarray, float] = 1.0, mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>, keep_max_value: bool = False, keep_min_value: bool = False):
382    def fill_by_boxes(
383        self,
384        boxes: Iterable['Box'],
385        value: Union['ScoreMap', np.ndarray, float] = 1.0,
386        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
387        keep_max_value: bool = False,
388        keep_min_value: bool = False,
389    ):
390        self.fill_by_box_value_pairs(
391            box_value_pairs=((box, value) for box in boxes),
392            mode=mode,
393            keep_max_value=keep_max_value,
394            keep_min_value=keep_min_value,
395            skip_values_uniqueness_check=True,
396        )
def fill_by_polygon_value_pairs( self, polygon_value_pairs: collections.abc.Iterable[tuple[vkit.element.polygon.Polygon, typing.Union[vkit.element.score_map.ScoreMap, numpy.ndarray, float]]], mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>, keep_max_value: bool = False, keep_min_value: bool = False, skip_values_uniqueness_check: bool = False):
398    def fill_by_polygon_value_pairs(
399        self,
400        polygon_value_pairs: Iterable[Tuple['Polygon', Union['ScoreMap', np.ndarray, float]]],
401        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
402        keep_max_value: bool = False,
403        keep_min_value: bool = False,
404        skip_values_uniqueness_check: bool = False,
405    ):
406        polygons, values = self.unpack_element_value_pairs(self.is_prob, polygon_value_pairs)
407
408        polygons_mask = generate_fill_by_polygons_mask(self.shape, polygons, mode)
409        if polygons_mask is None:
410            for polygon, value in zip(polygons, values):
411                polygon.fill_score_map(
412                    score_map=self,
413                    value=value,
414                    keep_max_value=keep_max_value,
415                    keep_min_value=keep_min_value,
416                )
417
418        else:
419            unique = True
420            if not skip_values_uniqueness_check:
421                unique = check_elements_uniqueness(values)
422
423            if unique:
424                polygons_mask.fill_score_map(
425                    score_map=self,
426                    value=values[0],
427                    keep_max_value=keep_max_value,
428                    keep_min_value=keep_min_value,
429                )
430            else:
431                for polygon, value in zip(polygons, values):
432                    bounding_box = polygon.to_bounding_box()
433                    polygon_mask = bounding_box.extract_mask(polygons_mask)
434                    polygon_mask = polygon_mask.to_box_attached(bounding_box)
435                    polygon_mask.fill_score_map(
436                        score_map=self,
437                        value=value,
438                        keep_max_value=keep_max_value,
439                        keep_min_value=keep_min_value,
440                    )
def fill_by_polygons( self, polygons: collections.abc.Iterable[vkit.element.polygon.Polygon], value: Union[vkit.element.score_map.ScoreMap, numpy.ndarray, float] = 1.0, mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>, keep_max_value: bool = False, keep_min_value: bool = False):
442    def fill_by_polygons(
443        self,
444        polygons: Iterable['Polygon'],
445        value: Union['ScoreMap', np.ndarray, float] = 1.0,
446        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
447        keep_max_value: bool = False,
448        keep_min_value: bool = False,
449    ):
450        self.fill_by_polygon_value_pairs(
451            polygon_value_pairs=((polygon, value) for polygon in polygons),
452            mode=mode,
453            keep_max_value=keep_max_value,
454            keep_min_value=keep_min_value,
455            skip_values_uniqueness_check=True,
456        )
def fill_by_mask_value_pairs( self, mask_value_pairs: collections.abc.Iterable[tuple[vkit.element.mask.Mask, typing.Union[vkit.element.score_map.ScoreMap, numpy.ndarray, float]]], mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>, keep_max_value: bool = False, keep_min_value: bool = False, skip_values_uniqueness_check: bool = False):
458    def fill_by_mask_value_pairs(
459        self,
460        mask_value_pairs: Iterable[Tuple['Mask', Union['ScoreMap', np.ndarray, float]]],
461        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
462        keep_max_value: bool = False,
463        keep_min_value: bool = False,
464        skip_values_uniqueness_check: bool = False,
465    ):
466        masks, values = self.unpack_element_value_pairs(self.is_prob, mask_value_pairs)
467
468        masks_mask = generate_fill_by_masks_mask(self.shape, masks, mode)
469        if masks_mask is None:
470            for mask, value in zip(masks, values):
471                mask.fill_score_map(
472                    score_map=self,
473                    value=value,
474                    keep_max_value=keep_max_value,
475                    keep_min_value=keep_min_value,
476                )
477
478        else:
479            unique = True
480            if not skip_values_uniqueness_check:
481                unique = check_elements_uniqueness(values)
482
483            if unique:
484                masks_mask.fill_score_map(
485                    score_map=self,
486                    value=values[0],
487                    keep_max_value=keep_max_value,
488                    keep_min_value=keep_min_value,
489                )
490            else:
491                for mask, value in zip(masks, values):
492                    if mask.box:
493                        boxed_mask = mask.box.extract_mask(masks_mask)
494                    else:
495                        boxed_mask = masks_mask
496
497                    boxed_mask = boxed_mask.copy()
498                    mask.to_inverted_mask().fill_mask(boxed_mask, value=0)
499                    boxed_mask.fill_score_map(
500                        score_map=self,
501                        value=value,
502                        keep_max_value=keep_max_value,
503                        keep_min_value=keep_min_value,
504                    )
def fill_by_masks( self, masks: collections.abc.Iterable[vkit.element.mask.Mask], value: Union[vkit.element.score_map.ScoreMap, numpy.ndarray, float] = 1.0, mode: vkit.element.type.ElementSetOperationMode = <ElementSetOperationMode.UNION: 'union'>, keep_max_value: bool = False, keep_min_value: bool = False):
506    def fill_by_masks(
507        self,
508        masks: Iterable['Mask'],
509        value: Union['ScoreMap', np.ndarray, float] = 1.0,
510        mode: ElementSetOperationMode = ElementSetOperationMode.UNION,
511        keep_max_value: bool = False,
512        keep_min_value: bool = False,
513    ):
514        self.fill_by_mask_value_pairs(
515            mask_value_pairs=((mask, value) for mask in masks),
516            mode=mode,
517            keep_max_value=keep_max_value,
518            keep_min_value=keep_min_value,
519            skip_values_uniqueness_check=True,
520        )
def fill_by_quad_interpolation( self, point0: vkit.element.point.Point, point1: vkit.element.point.Point, point2: vkit.element.point.Point, point3: vkit.element.point.Point, func_np_uv_to_mat: Callable[[numpy.ndarray], numpy.ndarray], keep_max_value: bool = False, keep_min_value: bool = False):
563    def fill_by_quad_interpolation(
564        self,
565        point0: 'Point',
566        point1: 'Point',
567        point2: 'Point',
568        point3: 'Point',
569        func_np_uv_to_mat: Callable[[np.ndarray], np.ndarray],
570        keep_max_value: bool = False,
571        keep_min_value: bool = False,
572    ):
573        score_map = self.from_quad_interpolation(
574            point0=point0,
575            point1=point1,
576            point2=point2,
577            point3=point3,
578            func_np_uv_to_mat=func_np_uv_to_mat,
579            is_prob=self.is_prob,
580        )
581        assert score_map.box
582        with self.writable_context:
583            score_map.box.fill_np_array(
584                mat=self.mat,
585                value=score_map.mat,
586                np_mask=(score_map.mat > 0.0),
587                keep_max_value=keep_max_value,
588                keep_min_value=keep_min_value,
589            )
def to_shifted_score_map(self, offset_y: int = 0, offset_x: int = 0):
591    def to_shifted_score_map(self, offset_y: int = 0, offset_x: int = 0):
592        assert self.box
593        shifted_box = self.box.to_shifted_box(offset_y=offset_y, offset_x=offset_x)
594        return attrs.evolve(self, box=shifted_box)
def to_conducted_resized_polygon( self, shapable_or_shape: Union[vkit.element.type.Shapable, Tuple[int, int]], resized_height: Union[int, NoneType] = None, resized_width: Union[int, NoneType] = None, cv_resize_interpolation: int = 2):
596    def to_conducted_resized_polygon(
597        self,
598        shapable_or_shape: Union[Shapable, Tuple[int, int]],
599        resized_height: Optional[int] = None,
600        resized_width: Optional[int] = None,
601        cv_resize_interpolation: int = cv.INTER_CUBIC,
602    ):
603        assert self.box
604        resized_box = self.box.to_conducted_resized_box(
605            shapable_or_shape=shapable_or_shape,
606            resized_height=resized_height,
607            resized_width=resized_width,
608        )
609        resized_score_map = self.to_box_detached().to_resized_score_map(
610            resized_height=resized_box.height,
611            resized_width=resized_box.width,
612            cv_resize_interpolation=cv_resize_interpolation,
613        )
614        resized_score_map = resized_score_map.to_box_attached(resized_box)
615        return resized_score_map
def to_resized_score_map( self, resized_height: Union[int, NoneType] = None, resized_width: Union[int, NoneType] = None, cv_resize_interpolation: int = 2):
617    def to_resized_score_map(
618        self,
619        resized_height: Optional[int] = None,
620        resized_width: Optional[int] = None,
621        cv_resize_interpolation: int = cv.INTER_CUBIC,
622    ):
623        assert not self.box
624        _, _, resized_height, resized_width = generate_shape_and_resized_shape(
625            shapable_or_shape=self.shape,
626            resized_height=resized_height,
627            resized_width=resized_width,
628        )
629        mat = cv.resize(
630            self.mat,
631            (resized_width, resized_height),
632            interpolation=cv_resize_interpolation,
633        )
634        if self.is_prob:
635            # NOTE: Interpolation like bi-cubic could generate out-of-bound values.
636            mat = np.clip(mat, 0.0, 1.0)
637        return attrs.evolve(self, mat=mat)
def to_cropped_score_map( self, up: Union[int, NoneType] = None, down: Union[int, NoneType] = None, left: Union[int, NoneType] = None, right: Union[int, NoneType] = None):
639    def to_cropped_score_map(
640        self,
641        up: Optional[int] = None,
642        down: Optional[int] = None,
643        left: Optional[int] = None,
644        right: Optional[int] = None,
645    ):
646        assert not self.box
647
648        up = up or 0
649        down = down or self.height - 1
650        left = left or 0
651        right = right or self.width - 1
652
653        return attrs.evolve(self, mat=self.mat[up:down + 1, left:right + 1])
def to_box_attached(self, box: vkit.element.box.Box):
655    def to_box_attached(self, box: 'Box'):
656        assert self.height == box.height
657        assert self.width == box.width
658        return attrs.evolve(self, box=box)
def to_box_detached(self):
660    def to_box_detached(self):
661        assert self.box
662        return attrs.evolve(self, box=None)
def fill_np_array( self, mat: numpy.ndarray, value: Union[numpy.ndarray, Tuple[float, ...], float], keep_max_value: bool = False, keep_min_value: bool = False):
664    def fill_np_array(
665        self,
666        mat: np.ndarray,
667        value: Union[np.ndarray, Tuple[float, ...], float],
668        keep_max_value: bool = False,
669        keep_min_value: bool = False,
670    ):
671        self.equivalent_box.fill_np_array(
672            mat=mat,
673            value=value,
674            alpha=self,
675            keep_max_value=keep_max_value,
676            keep_min_value=keep_min_value,
677        )
def fill_image( self, image: vkit.element.image.Image, value: Union[vkit.element.image.Image, numpy.ndarray, Tuple[int, ...], int]):
679    def fill_image(
680        self,
681        image: 'Image',
682        value: Union['Image', np.ndarray, Tuple[int, ...], int],
683    ):
684        self.equivalent_box.fill_image(
685            image=image,
686            value=value,
687            alpha=self,
688        )
def to_mask(self, threshold: float = 0.0):
690    def to_mask(self, threshold: float = 0.0):
691        mat = (self.mat > threshold).astype(np.uint8)
692        return Mask(mat=mat, box=self.box)
def generate_fill_by_score_maps_mask( shape: Tuple[int, int], score_maps: Iterable[vkit.element.score_map.ScoreMap], mode: vkit.element.type.ElementSetOperationMode):
695def generate_fill_by_score_maps_mask(
696    shape: Tuple[int, int],
697    score_maps: Iterable[ScoreMap],
698    mode: ElementSetOperationMode,
699):
700    if mode == ElementSetOperationMode.UNION:
701        return None
702    else:
703        return Mask.from_score_maps(shape, score_maps, mode)