notebooks/util/postproc/boxes.py [11:229]:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
class UniversalBox:
    """Box class with forgiving/flexible constructor(s) and useful props/serialization options

    No more getting bogged down translating between different formats for representing boxes!
    """

    def __init__(
        self,
        top: Optional[Real] = None,
        left: Optional[Real] = None,
        height: Optional[Real] = None,
        width: Optional[Real] = None,
        bottom: Optional[Real] = None,
        right: Optional[Real] = None,
        box: Optional[Any] = None,
        inverted_y: bool = True,
    ):
        """Create a UniversalBox from some kind of bounding box definition

        You can provide whatever sufficient combination of {top,left,height,width,bottom,right}
        keyword args you like; *OR* a `box` object with equivalent attributes - which may be
        PascalCase e.g. box.Height or box.height. `box` can also be a dict.

        `inverted_y` controls whether coordinates are image-style (default, bottom = top + height)
        or math-style (top = height + bottom).
        """

        def get_box_attr(o, attr_lower: str):
            if not o:
                return o
            if hasattr(o, attr_lower):
                return getattr(o, attr_lower)
            attr_pascal = attr_lower[0].upper() + attr_lower[1:]
            if hasattr(o, "get"):
                val = o.get(attr_lower)
                if val is None:
                    val = o.get(attr_pascal)
            else:
                val = None
            return val

        self.inverted_y = inverted_y
        self._top = get_box_attr(box, "top") if top is None else top
        self._height = get_box_attr(box, "height") if height is None else height
        self._bottom = get_box_attr(box, "bottom") if bottom is None else bottom

        self._left = get_box_attr(box, "left") if left is None else left
        self._width = get_box_attr(box, "width") if width is None else width
        self._right = get_box_attr(box, "right") if right is None else right

        if sum(map(lambda v: v is None, (self._top, self._bottom, self._height))) > 1:
            raise ValueError(
                "At least 2 of [top, height, bottom] must be specified. Got [{}, {}, {}]",
                self._top,
                self._height,
                self._bottom,
            )
        if self._top is None:
            self._top = (
                (self._bottom - self._height) if inverted_y else (self._bottom + self._height)
            )
        if self._bottom is None:
            self._bottom = (self._top + self._height) if inverted_y else (self._top - self._height)
        expected_height = (self._bottom - self._top) if inverted_y else (self._top - self._bottom)
        if self._height is None:
            self._height = expected_height
        elif self._height != expected_height:
            raise ValueError(
                "Specified height {} does not match specified top {} and bottom {}".format(
                    self._height,
                    self._top,
                    self._bottom,
                )
            )

        if sum(map(lambda v: v is None, (self._left, self._width, self._right))) > 1:
            raise ValueError(
                "At least 2 of [left, width, right] must be specified. Got [{}, {}, {}]",
                self._left,
                self._width,
                self._right,
            )
        if self._left is None:
            self._left = self._right - self._width
        if self._right is None:
            self._right = self._left + self._width
        expected_width = self._right - self._left
        if self._width is None:
            self._width = expected_width
        elif self._width != expected_width:
            raise ValueError(
                "Specified width {} does not match specified right {} - left {} = {}".format(
                    self._width,
                    self._right,
                    self._left,
                    expected_width,
                )
            )

    @property
    def top(self) -> Real:
        return self._top

    @top.setter
    def top(self, value: Real) -> None:
        self._height = self._bottom - value if self.inverted_y else self._bottom + value
        self._top = value

    @property
    def left(self) -> Real:
        return self._left

    @left.setter
    def left(self, value: Real) -> None:
        self._width = self._right - value
        self._left = value

    @property
    def height(self) -> Real:
        return self._height

    @property
    def width(self) -> Real:
        return self._width

    @property
    def bottom(self) -> Real:
        return self._bottom

    @bottom.setter
    def bottom(self, value: Real) -> None:
        self._height = self._top + value if self.inverted_y else self._top - value
        self._bottom = value

    @property
    def right(self) -> Real:
        return self._right

    @right.setter
    def right(self, value: Real) -> None:
        self._width = self._left + value
        self._right = value

    def to_dict(self, style: str = "TLHW") -> Dict[str, Real]:
        """Express the box as a (JSON serializable) dict

        Arguments
        ---------
        style : str
            Some combination of characters T,L,H,W,B,R (upper- or lower-case) indicating what
            properties should be included in the dict. E.g. 'TLbr' will generate a result with
            { 'Top', 'Left', 'bottom', 'right' }
        """
        if not style:
            return ValueError(f"Bounding box to_dict got empty style spec '{style}'")

        result = {}
        for prop in style:
            if prop == "T":
                result["Top"] = self._top
            elif prop == "t":
                result["top"] = self._top
            elif prop == "L":
                result["Left"] = self._left
            elif prop == "l":
                result["left"] = self._left
            elif prop == "H":
                result["Height"] = self._height
            elif prop == "h":
                result["height"] = self._height
            elif prop == "W":
                result["Width"] = self._width
            elif prop == "w":
                result["width"] = self._width
            elif prop == "B":
                result["Bottom"] = self._bottom
            elif prop == "b":
                result["bottom"] = self._bottom
            elif prop == "R":
                result["Right"] = self._right
            elif prop == "r":
                result["right"] = self._right
            else:
                raise ValueError(
                    f"Bounding box to_dict style '{style}' contained unrecognised spec '{prop}'"
                )
        return result

    @classmethod
    def aggregate(
        cls: Type[UniversalBox],
        boxes: Iterable[UniversalBox],
        inverted_y: Optional[bool] = None,
    ) -> UniversalBox:
        """Calculate the minimal bounding box containing input `boxes`

        Arguments
        ---------
        boxes : Iterable[UniversalBox]
            The UniversalBox instances to combine
        inverted_y : Optional[bool]
            If not provided, will be inferred from the `boxes`
        """
        if not (boxes and len(boxes)):
            raise ValueError(f"Cannot aggregate with no 'boxes'! Got {boxes}")

        if inverted_y is None:
            n_inverted_ys = sum(b.inverted_y for b in boxes)
            inverted_y = n_inverted_ys > (len(boxes) / 2)

        box_tops = [b.top if b.inverted_y == inverted_y else b.bottom for b in boxes]
        box_bottoms = [b.bottom if b.inverted_y == inverted_y else b.top for b in boxes]
        return cls(
            top=min(box_tops) if inverted_y else max(box_tops),
            bottom=max(box_bottoms) if inverted_y else min(box_bottoms),
            left=min(b.left for b in boxes),
            right=max(b.right for b in boxes),
            inverted_y=inverted_y,
        )
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -



pipeline/postprocessing/fn-postprocess/util/boxes.py [11:229]:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
class UniversalBox:
    """Box class with forgiving/flexible constructor(s) and useful props/serialization options

    No more getting bogged down translating between different formats for representing boxes!
    """

    def __init__(
        self,
        top: Optional[Real] = None,
        left: Optional[Real] = None,
        height: Optional[Real] = None,
        width: Optional[Real] = None,
        bottom: Optional[Real] = None,
        right: Optional[Real] = None,
        box: Optional[Any] = None,
        inverted_y: bool = True,
    ):
        """Create a UniversalBox from some kind of bounding box definition

        You can provide whatever sufficient combination of {top,left,height,width,bottom,right}
        keyword args you like; *OR* a `box` object with equivalent attributes - which may be
        PascalCase e.g. box.Height or box.height. `box` can also be a dict.

        `inverted_y` controls whether coordinates are image-style (default, bottom = top + height)
        or math-style (top = height + bottom).
        """

        def get_box_attr(o, attr_lower: str):
            if not o:
                return o
            if hasattr(o, attr_lower):
                return getattr(o, attr_lower)
            attr_pascal = attr_lower[0].upper() + attr_lower[1:]
            if hasattr(o, "get"):
                val = o.get(attr_lower)
                if val is None:
                    val = o.get(attr_pascal)
            else:
                val = None
            return val

        self.inverted_y = inverted_y
        self._top = get_box_attr(box, "top") if top is None else top
        self._height = get_box_attr(box, "height") if height is None else height
        self._bottom = get_box_attr(box, "bottom") if bottom is None else bottom

        self._left = get_box_attr(box, "left") if left is None else left
        self._width = get_box_attr(box, "width") if width is None else width
        self._right = get_box_attr(box, "right") if right is None else right

        if sum(map(lambda v: v is None, (self._top, self._bottom, self._height))) > 1:
            raise ValueError(
                "At least 2 of [top, height, bottom] must be specified. Got [{}, {}, {}]",
                self._top,
                self._height,
                self._bottom,
            )
        if self._top is None:
            self._top = (
                (self._bottom - self._height) if inverted_y else (self._bottom + self._height)
            )
        if self._bottom is None:
            self._bottom = (self._top + self._height) if inverted_y else (self._top - self._height)
        expected_height = (self._bottom - self._top) if inverted_y else (self._top - self._bottom)
        if self._height is None:
            self._height = expected_height
        elif self._height != expected_height:
            raise ValueError(
                "Specified height {} does not match specified top {} and bottom {}".format(
                    self._height,
                    self._top,
                    self._bottom,
                )
            )

        if sum(map(lambda v: v is None, (self._left, self._width, self._right))) > 1:
            raise ValueError(
                "At least 2 of [left, width, right] must be specified. Got [{}, {}, {}]",
                self._left,
                self._width,
                self._right,
            )
        if self._left is None:
            self._left = self._right - self._width
        if self._right is None:
            self._right = self._left + self._width
        expected_width = self._right - self._left
        if self._width is None:
            self._width = expected_width
        elif self._width != expected_width:
            raise ValueError(
                "Specified width {} does not match specified right {} - left {} = {}".format(
                    self._width,
                    self._right,
                    self._left,
                    expected_width,
                )
            )

    @property
    def top(self) -> Real:
        return self._top

    @top.setter
    def top(self, value: Real) -> None:
        self._height = self._bottom - value if self.inverted_y else self._bottom + value
        self._top = value

    @property
    def left(self) -> Real:
        return self._left

    @left.setter
    def left(self, value: Real) -> None:
        self._width = self._right - value
        self._left = value

    @property
    def height(self) -> Real:
        return self._height

    @property
    def width(self) -> Real:
        return self._width

    @property
    def bottom(self) -> Real:
        return self._bottom

    @bottom.setter
    def bottom(self, value: Real) -> None:
        self._height = self._top + value if self.inverted_y else self._top - value
        self._bottom = value

    @property
    def right(self) -> Real:
        return self._right

    @right.setter
    def right(self, value: Real) -> None:
        self._width = self._left + value
        self._right = value

    def to_dict(self, style: str = "TLHW") -> Dict[str, Real]:
        """Express the box as a (JSON serializable) dict

        Arguments
        ---------
        style : str
            Some combination of characters T,L,H,W,B,R (upper- or lower-case) indicating what
            properties should be included in the dict. E.g. 'TLbr' will generate a result with
            { 'Top', 'Left', 'bottom', 'right' }
        """
        if not style:
            return ValueError(f"Bounding box to_dict got empty style spec '{style}'")

        result = {}
        for prop in style:
            if prop == "T":
                result["Top"] = self._top
            elif prop == "t":
                result["top"] = self._top
            elif prop == "L":
                result["Left"] = self._left
            elif prop == "l":
                result["left"] = self._left
            elif prop == "H":
                result["Height"] = self._height
            elif prop == "h":
                result["height"] = self._height
            elif prop == "W":
                result["Width"] = self._width
            elif prop == "w":
                result["width"] = self._width
            elif prop == "B":
                result["Bottom"] = self._bottom
            elif prop == "b":
                result["bottom"] = self._bottom
            elif prop == "R":
                result["Right"] = self._right
            elif prop == "r":
                result["right"] = self._right
            else:
                raise ValueError(
                    f"Bounding box to_dict style '{style}' contained unrecognised spec '{prop}'"
                )
        return result

    @classmethod
    def aggregate(
        cls: Type[UniversalBox],
        boxes: Iterable[UniversalBox],
        inverted_y: Optional[bool] = None,
    ) -> UniversalBox:
        """Calculate the minimal bounding box containing input `boxes`

        Arguments
        ---------
        boxes : Iterable[UniversalBox]
            The UniversalBox instances to combine
        inverted_y : Optional[bool]
            If not provided, will be inferred from the `boxes`
        """
        if not (boxes and len(boxes)):
            raise ValueError(f"Cannot aggregate with no 'boxes'! Got {boxes}")

        if inverted_y is None:
            n_inverted_ys = sum(b.inverted_y for b in boxes)
            inverted_y = n_inverted_ys > (len(boxes) / 2)

        box_tops = [b.top if b.inverted_y == inverted_y else b.bottom for b in boxes]
        box_bottoms = [b.bottom if b.inverted_y == inverted_y else b.top for b in boxes]
        return cls(
            top=min(box_tops) if inverted_y else max(box_tops),
            bottom=max(box_bottoms) if inverted_y else min(box_bottoms),
            left=min(b.left for b in boxes),
            right=max(b.right for b in boxes),
            inverted_y=inverted_y,
        )
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -



