# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/10a_bbox.core.ipynb.

# %% auto 0
__all__ = ['BBox', 'OBBox', 'BBoxLabeled', 'OBBoxLabeled']

# %% ../../nbs/10a_bbox.core.ipynb 3
from fastcore.all import *
import math
import numpy as np
import polvo as pv

# %% ../../nbs/10a_bbox.core.ipynb 5
class _BBox:
    def __init__(self, points):
        self.points, _ = pv.sort_quadrilateral(np.array(points))
        
    def normalized_points(self, w, h): return self.points / np.array((w, h))
    
    @property
    def xyxyxyxy(self): return self.points
    @property
    def flat(self): return self.points.flatten()
    @property
    def xmin(self): return self.points[:, 0].min()
    @property
    def xmax(self): return self.points[:, 0].max()
    @property
    def ymin(self): return self.points[:, 1].min()
    @property
    def ymax(self): return self.points[:, 1].max()
    @property
    def width(self): return self.xmax-self.xmin
    @property
    def height(self): return self.ymax-self.ymin
    @property
    def area(self): return self.width*self.height
    @property
    def xyxy(self): return np.array((self.xmin, self.ymin, self.xmax, self.ymax))
    @property
    def yxyx(self): return np.array((self.ymin, self.xmin, self.ymax, self.xmax))
    @property
    def xywh(self): return np.array((self.xmin, self.ymin, self.width, self.height))
    @property
    def xycenter_wh(self): return np.array((self.xmin+self.width/2, self.ymin+self.height/2, self.width, self.height))
        
    @classmethod
    def from_points(cls, points): return cls(points)
    @classmethod
    def from_flat(cls, points): return cls(np.reshape(points, (4, 2)))

# %% ../../nbs/10a_bbox.core.ipynb 6
class BBox(_BBox):
    """Bounding Box representation.
    Should **not** be instantiated directly, instead use `from_*` methods. e.g. `from_xyxy`, `from_xywh`.
    Is able to transform coordinates into different formats, e.g. `xyxy`, `xywh`.
    
    # Examples
    Create from `xywh` format, and get `xyxy` coordinates.
    ```python
    bbox = BBox.from_xywh(1, 1, 4, 4)
    xyxy = bbox.xyxy
    ```
    """
    def __repr__(self):
        return f"<{self.__class__.__name__} (xmin:{self.xmin}, ymin:{self.ymin}, xmax:{self.xmax}, ymax:{self.ymax})>"

    def __eq__(self, other) -> bool:
        if isinstance(other, BBox): return self.xyxyxyxy == other.xyxyxyxy
        return False
    
    def accept_visit(self, visitor): return visitor.visit_bbox(self)

    def relative_xcycwh(self, img_width: int, img_height: int):
        scale = np.array([img_width, img_height, img_width, img_height])
        x, y, w, h = self.xywh / scale
        xc = x + 0.5*w
        yc = y + 0.5*h
        return (xc, yc, w, h)
    
    def normalized_xycenter_wh(self, img_width: int, img_height: int):
        scale = np.array([img_width, img_height, img_width, img_height])
        return self.xycenter_wh / scale

    @classmethod
    def from_xyxy(cls, xmin, ymin, xmax, ymax): return cls.from_flat((xmin,ymin, xmax,ymin, xmax,ymax, xmin,ymax))
    @classmethod
    def from_xywh(cls, x, y, w, h): return cls.from_xyxy(x, y, x+w, y+h)
    @classmethod
    def from_xycenter_wh(cls, xc, yc, w, h):
        return cls.from_xywh(xc-w/2, yc-h/2, w, h)
    @classmethod
    def from_relative_xcycwh(cls, xc, yc, bw, bh, img_width, img_height):
        # subtracting 0.5 goes from center to left/upper edge, adding goes to right/bottom
        pnts = [(xc - 0.5*bw), (yc - 0.5*bh), (xc + 0.5*bw), (yc + 0.5*bh)]
        # convert from relative to absolute coordinates
        scale = np.array([img_width, img_height, img_width, img_height])
        xmin, ymin, xmax, ymax = np.around(pnts * scale).astype(int).tolist()
        return cls.from_xyxy(xmin, ymin, xmax, ymax)
    @classmethod
    def from_rle(cls, rle, h, w):
        a = np.array(rle.counts, dtype=int)
        a = a.reshape((-1, 2))  # an array of (start, length) pairs
        a[:, 0] -= 1  # `start` is 1-indexed
        y0 = a[:, 0] % h
        y1 = y0 + a[:, 1]
        if np.any(y1 > h):
            # got `y` overrun, meaning that there are a pixels in mask on 0 and shape[0] position
            y0 = 0
            y1 = h
        else:
            y0 = np.min(y0)
            y1 = np.max(y1)
        x0 = a[:, 0] // h
        x1 = (a[:, 0] + a[:, 1]) // h
        x0 = np.min(x0)
        x1 = np.max(x1)
        if x1 > w:
            # just went out of the image dimensions
            raise ValueError(f"invalid RLE or image dimensions: x1={x1} > shape[1]={w}")
        return cls.from_xyxy(x0, y0, x1, y1)

# %% ../../nbs/10a_bbox.core.ipynb 12
class OBBox(_BBox):
    @classmethod
    def from_clockwise(cls, x,y, w,h, degrees):
        r = math.radians(degrees)
        sinr, cosr = math.sin(r), math.cos(r)
        x2 = x+cosr*w
        y2 = y+sinr*w
        x3 = x+cosr*w-sinr*h
        y3 = y+sinr*w+cosr*h
        x4 = x-sinr*h
        y4 = y+cosr*h
        
        return cls.from_flat((x,y, x2,y2, x3,y3, x4,y4))
    
    def accept_visit(self, visitor): return visitor.visit_obbox(self)
    
    # TODO: when angle conversion is implemented, use it here
    def __repr__(self): return f"<{self.__class__.__name__} ({self.points})>"

# %% ../../nbs/10a_bbox.core.ipynb 15
class _BBoxLabeled(GetAttr):
    _default='bbox'
    def __init__(self, bbox, label): store_attr()
    def __repr__(self): return f'{self.label} | {self.bbox}'

# %% ../../nbs/10a_bbox.core.ipynb 16
class BBoxLabeled(_BBoxLabeled):
    def accept_visit(self, visitor): return visitor.visit_bbox_labelled(self)

# %% ../../nbs/10a_bbox.core.ipynb 18
class OBBoxLabeled(_BBoxLabeled):
    def accept_visit(self, visitor): return visitor.visit_obbox_labelled(self)
