Skip to content

box_function

data.transform.box_function

PolygonType module-attribute

PolygonType = List[ndarray]

BBoxesType module-attribute

BBoxesType = Union[ndarray, NumpyBoxes]

OffsetType module-attribute

OffsetType = Tuple[float, float]

NumpyBoxes

Bases: ndarray

rotate_polygons

rotate_polygons(polygons: PolygonType, angle: float, image_size: Tuple[int], target_size: Optional[Tuple[int]] = None) -> PolygonType
Source code in SaigeToolkit/data/transform/polygon_function.py
def rotate(
    polygons: PolygonType,
    angle: float,
    image_size: Tuple[int],
    target_size: Optional[Tuple[int]] = None,
) -> PolygonType:
    if len(polygons) == 0:
        return polygons

    dtype = polygons[0].dtype
    w, h = image_size

    anchor_origin = np.array([w // 2, h // 2]).astype(dtype)

    if target_size is None:
        anchor_target = anchor_origin
    else:
        anchor_target = np.array([target_size[0] // 2, target_size[1] // 2]).astype(dtype)

    theta = np.deg2rad(angle)
    rotation_matrix = [[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]]

    rotated_polygons = []
    for polygon in polygons:
        # Translate polygon so that the rotation point is at the origin
        rotated_polygon = polygon - anchor_origin

        # Rotate the polygon
        rotated_polygon = np.dot(rotated_polygon, rotation_matrix)

        # Translate the polygon back
        rotated_polygon += anchor_target

        rotated_polygons.append(rotated_polygon)

    return rotated_polygons

calculate_transform_matrix

calculate_transform_matrix(width: int, height: int, offset_top_left: OffsetType, offset_top_right: OffsetType, offset_bottom_right: OffsetType, offset_bottom_left: OffsetType) -> ndarray

Calculates the perspective transformation matrix using the given width, height, and corner points of a rectangle.

Parameters:

  • width (int) –

    Width of the original image.

  • height (int) –

    Height of the original image.

  • offset_top_left (OffsetType) –

    The offset ratio of the top-left corner point. Represented by the coordinates (x, y) and has a range of [0, 49]. Calculate xand y according to the procedure below.

    x` = width * offset_top_left[0]
    y` = height * offset_top_left[1]
    
  • offset_bottom_right (OffsetType) –

    The offset ratio of the bottom-right corner point. Calculate xand y according to the procedure below.

    x` = width - width * offset_bottom_right[0]
    y` = hegiht - height * offset_bottom_right[1]
    
  • offset_top_right (OffsetType) –

    The offset ratio of the top-left corner point.

  • offset_bottom_left (OffsetType) –

    The offset ratio of the bottom-left corner point.

Returns:

  • ndarray

    np.ndarray: The 4x3 perspective transform matrix.

Source code in SaigeToolkit/data/transform/augmentation/augment_function.py
def calculate_transform_matrix(
    width: int,
    height: int,
    offset_top_left: OffsetType,
    offset_top_right: OffsetType,
    offset_bottom_right: OffsetType,
    offset_bottom_left: OffsetType,
) -> np.ndarray:
    """
    Calculates the perspective transformation matrix
    using the given width, height, and corner points of a rectangle.

    Parameters:
        width (int): Width of the original image.
        height (int): Height of the original image.
        offset_top_left (OffsetType):
            The offset ratio of the top-left corner point.
            Represented by the coordinates (x, y) and has a range of [0, 49].
            Calculate x` and y` according to the procedure below.

            ```
            x` = width * offset_top_left[0]
            y` = height * offset_top_left[1]
            ```

        offset_bottom_right (OffsetType):
            The offset ratio of the bottom-right corner point.
            Calculate x` and y` according to the procedure below.

            ```
            x` = width - width * offset_bottom_right[0]
            y` = hegiht - height * offset_bottom_right[1]
            ```

        offset_top_right (OffsetType): The offset ratio of the top-left corner point.
        offset_bottom_left (OffsetType): The offset ratio of the bottom-left corner point.

    Returns:
        np.ndarray: The 4x3 perspective transform matrix.

    """
    points_from = np.float32([[0, 0], [width, 0], [width, height], [0, height]])

    point_top_left: OffsetType = (
        (offset_top_left[0] / 100) * width,
        (offset_top_left[1] / 100) * height,
    )
    point_top_right: OffsetType = (
        width - (offset_top_right[0] / 100) * width,
        (offset_top_right[1] / 100) * height,
    )
    point_bottom_right: OffsetType = (
        width - (offset_bottom_right[0] / 100) * width,
        height - (offset_bottom_right[1] / 100) * height,
    )
    point_bottom_left: OffsetType = (
        (offset_bottom_left[0] / 100) * width,
        height - (offset_bottom_left[1] / 100) * height,
    )
    points_to = np.float32(
        [
            point_top_left,
            point_top_right,
            point_bottom_right,
            point_bottom_left,
        ]
    )
    transform_matrix = cv2.getPerspectiveTransform(points_from, points_to)

    return transform_matrix

check_value

check_value(data: Union[int, float], min_value: Union[int, float], max_value: Union[int, float])
Source code in SaigeToolkit/data/transform/function_util.py
def check_value(data: Union[int, float], min_value: Union[int, float], max_value: Union[int, float]):
    if not (isinstance(data, (int, float))):
        raise AugmentationParameterTypeError

    if not (min_value <= data <= max_value):
        raise AugmentationParameterRangeError

preserve_coordinates

preserve_coordinates(func)

Box augmentation이 (left, top, right, bottom) coordinate system을 기반으로 구현 되어있기 때문에, input bboxes의 coordinate system을 확인하고 augmentation에 맞는 coordinate system으로 변환하고, augmentation이 끝나면 다시 기존 coordinate system으로 변경하여 출력합니다.

  • np.ndarray의 경우 coordinate system을 체크할 수 없기 때문에 (left, top, right, bottom) coordinate system이라고 가정합니다.

  • NumpyBBoxes의 경우 NumpyBBoxes 내부 변수 coordinate과 내부 함수 convert_coordinate를 활용하여 구현됩니다.

Source code in SaigeToolkit/data/transform/box_function.py
def preserve_coordinates(func):
    """
    Box augmentation이 (left, top, right, bottom) coordinate system을 기반으로 구현 되어있기 때문에,
    input bboxes의 coordinate system을 확인하고 augmentation에 맞는 coordinate system으로 변환하고,
    augmentation이 끝나면 다시 기존 coordinate system으로 변경하여 출력합니다.

    - np.ndarray의 경우
    coordinate system을 체크할 수 없기 때문에 (left, top, right, bottom) coordinate system이라고 가정합니다.

    - NumpyBBoxes의 경우
    NumpyBBoxes 내부 변수 coordinate과 내부 함수 convert_coordinate를 활용하여 구현됩니다.

    """
    _FUNCTIONAL_BBOX_COORDINATE = "xyxy"

    def wrapped_function_for_ndarray(bboxes: np.ndarray, *args, **kwargs) -> np.ndarray:
        new_bboxes = func(bboxes, *args, **kwargs)

        return new_bboxes

    def wrapped_function_for_numpyboxes(bboxes: NumpyBoxes, *args, **kwargs) -> NumpyBoxes:
        # original coordinate -> (left, top, right, bottom)
        original_coordinate = bboxes.coordinate  # record original coordinate
        bboxes = bboxes.convert_coordinate(_FUNCTIONAL_BBOX_COORDINATE)  # -> (left, top, right, bottom)

        # box augmentation
        new_bboxes = func(bboxes, *args, **kwargs)
        if not isinstance(new_bboxes, bboxes.__class__):
            new_bboxes = bboxes.__class__(new_bboxes, _FUNCTIONAL_BBOX_COORDINATE)

        # (left, top, right, bottom) -> original coordinate
        new_bboxes = new_bboxes.convert_coordinate(original_coordinate)

        return new_bboxes

    @wraps(func)
    def wrapped_function(bboxes: BBoxesType, *args, **kwargs) -> BBoxesType:
        # isinstance가 상속된 type도 true를 return 하기 때문에 여기서는 type(instance) == class로 체크
        if isinstance(bboxes, NumpyBoxes):
            new_bboxes = wrapped_function_for_numpyboxes(bboxes, *args, **kwargs)
        elif isinstance(bboxes, np.ndarray):
            new_bboxes = wrapped_function_for_ndarray(bboxes, *args, **kwargs)
        else:
            raise NotImplementedError

        return new_bboxes

    return wrapped_function

resize_box

resize_box(bboxes: BBoxesType, image_size: Tuple[int], tw: int, th: int) -> BBoxesType

bbox resize from (w, h) to (tw, th)

Source code in SaigeToolkit/data/transform/box_function.py
@preserve_coordinates
def resize_box(bboxes: BBoxesType, image_size: Tuple[int], tw: int, th: int) -> BBoxesType:
    """bbox resize from (w, h) to (tw, th)"""
    dtype = bboxes.dtype
    w, h = image_size
    if w == tw and h == th:
        return bboxes
    else:
        w_scale = tw / w
        h_scale = th / h
        return (bboxes * (w_scale, h_scale, w_scale, h_scale)).astype(dtype)

crop_box

crop_box(bboxes: BBoxesType, cropping_box: Union[ndarray, List[int]]) -> BBoxesType

bbox crop. An image is cropped at (new_left, new_top, new_right, new_bottom)

Source code in SaigeToolkit/data/transform/box_function.py
@preserve_coordinates
def crop_box(bboxes: BBoxesType, cropping_box: Union[np.ndarray, List[int]]) -> BBoxesType:
    """bbox crop. An image is cropped at (new_left, new_top, new_right, new_bottom)"""
    dtype = bboxes.dtype
    new_left, new_top, new_right, new_bottom = map(round, cropping_box)
    new_h = new_bottom - new_top
    new_bboxes = bboxes - (new_left, new_top, new_left, new_top)
    new_bboxes[:, [0, 1]] = np.maximum(new_bboxes[:, [0, 1]], 0)
    new_bboxes[:, [2, 3]] = np.minimum(new_bboxes[:, [2, 3]], (new_right - new_left, new_h))
    return new_bboxes.astype(dtype)

hflip_box

hflip_box(bboxes: BBoxesType, image_size: Tuple[int]) -> BBoxesType
Source code in SaigeToolkit/data/transform/box_function.py
@preserve_coordinates
def hflip_box(bboxes: BBoxesType, image_size: Tuple[int]) -> BBoxesType:
    w, h = image_size
    bboxes = bboxes.copy()
    bboxes[:, [0, 2]] = w - bboxes[:, [2, 0]]
    return bboxes

vflip_box

vflip_box(bboxes: BBoxesType, image_size: Tuple[int]) -> BBoxesType
Source code in SaigeToolkit/data/transform/box_function.py
@preserve_coordinates
def vflip_box(bboxes: BBoxesType, image_size: Tuple[int]) -> BBoxesType:
    w, h = image_size
    bboxes = bboxes.copy()
    bboxes[:, [1, 3]] = h - bboxes[:, [3, 1]]
    return bboxes

rotate

rotate(bboxes: BBoxesType, angle: Union[float, int], image_size: Tuple[int], target_size: Optional[Tuple[int]] = None, clipping: bool = True) -> BBoxesType
Source code in SaigeToolkit/data/transform/box_function.py
@preserve_coordinates
def rotate(
    bboxes: BBoxesType,
    angle: Union[float, int],
    image_size: Tuple[int],
    target_size: Optional[Tuple[int]] = None,
    clipping: bool = True,
) -> BBoxesType:
    dtype = bboxes.dtype
    w, h = image_size

    def _bboxes_to_polygons(bboxes: BBoxesType) -> PolygonType:
        polygons = []
        for bbox in bboxes:
            x1, y1, x2, y2 = bbox
            polygon = np.array([[x1, y1], [x1, y2], [x2, y2], [x2, y1]]).astype(dtype)
            polygons.append(polygon)
        return polygons

    def _polygons_to_bboxes(polygons: PolygonType) -> BBoxesType:
        bboxes = []
        for polygon in polygons:
            # Extract the bounding box from the rotated polygon
            x1, y1 = np.min(polygon, axis=0)
            x2, y2 = np.max(polygon, axis=0)
            bboxes.append([x1, y1, x2, y2])

        bboxes = np.array(bboxes).reshape(-1, 4)
        return bboxes

    if bboxes.size == 0:
        return bboxes

    polygons = _bboxes_to_polygons(bboxes)
    rotated_polygons = rotate_polygons(
        polygons=polygons, angle=angle, image_size=image_size, target_size=target_size
    )
    rotated_bboxes = _polygons_to_bboxes(rotated_polygons)

    if clipping:
        rotated_bboxes[:, [0, 2]] = np.clip(rotated_bboxes[:, [0, 2]], 0, w - 1)
        rotated_bboxes[:, [1, 3]] = np.clip(rotated_bboxes[:, [1, 3]], 0, h - 1)
    return rotated_bboxes.astype(dtype)

rotate90

rotate90(bboxes: BBoxesType, factor: int, image_size: Tuple[int], clipping: bool = True) -> BBoxesType
Source code in SaigeToolkit/data/transform/box_function.py
@preserve_coordinates
def rotate90(
    bboxes: BBoxesType,
    factor: int,
    image_size: Tuple[int],
    clipping: bool = True,
) -> BBoxesType:
    angle = factor * 90.0
    if factor % 2 == 0:
        target_size = image_size
    else:
        target_size = (image_size[1], image_size[0])
    return rotate(
        bboxes=bboxes, angle=angle, image_size=image_size, target_size=target_size, clipping=clipping
    )

translate_box

translate_box(bboxes: BBoxesType, offset: Tuple[int], image_size: Tuple[int]) -> BBoxesType
Source code in SaigeToolkit/data/transform/box_function.py
@preserve_coordinates
def translate_box(bboxes: BBoxesType, offset: Tuple[int], image_size: Tuple[int]) -> BBoxesType:
    w, h = image_size
    x_offset, y_offset = offset
    new_bboxes = bboxes.copy()
    new_bboxes[:, 0] = np.maximum(np.minimum(bboxes[:, 0] - x_offset, w), 0)
    new_bboxes[:, 1] = np.maximum(np.minimum(bboxes[:, 1] - y_offset, h), 0)
    new_bboxes[:, 2] = np.minimum(np.maximum(bboxes[:, 2] - x_offset, 0), w)
    new_bboxes[:, 3] = np.minimum(np.maximum(bboxes[:, 3] - y_offset, 0), h)
    return new_bboxes

ratio_jitter

ratio_jitter(bboxes: BBoxesType, image_size: Tuple[int], proportion_left: float = 0.0, proportion_right: float = 0.0, proportion_top: float = 0.0, proportion_bottom: float = 0.0) -> BBoxesType
Source code in SaigeToolkit/data/transform/box_function.py
@preserve_coordinates
def ratio_jitter(
    bboxes: BBoxesType,
    image_size: Tuple[int],
    proportion_left: float = 0.0,
    proportion_right: float = 0.0,
    proportion_top: float = 0.0,
    proportion_bottom: float = 0.0,
) -> BBoxesType:
    w, h = image_size

    left = int(w * proportion_left)
    right = int(w * proportion_right)
    top = int(h * proportion_top)
    bottom = int(h * proportion_bottom)

    # Crop
    crop_left = left if left > 0 else 0
    crop_right = right if right > 0 else 0
    crop_top = top if top > 0 else 0
    crop_bottom = bottom if bottom > 0 else 0

    crop_w = max(w - crop_left - crop_right, 1)
    crop_h = max(h - crop_top - crop_bottom, 1)

    bboxes = crop_box(bboxes, [crop_left, crop_top, crop_left + crop_w, crop_top + crop_h])

    # Padding
    pad_left = 0 if left > 0 else -left
    pad_right = 0 if right > 0 else -right
    pad_top = 0 if top > 0 else -top
    pad_bottom = 0 if bottom > 0 else -bottom

    bboxes = resize_box(bboxes, (crop_w + pad_left + pad_right, crop_h + pad_top + pad_bottom), w, h)

    # Translate
    w_scale = (crop_w + pad_left + pad_right) / w
    h_scale = (crop_h + pad_top + pad_bottom) / h
    bboxes = translate_box(bboxes, [-pad_left / w_scale, -pad_top / h_scale], image_size)

    return bboxes

zoom

zoom(bboxes: BBoxesType, image_size: Tuple[int], ratio: float = 1.0, h_start: float = 0.0, w_start: float = 0.0) -> BBoxesType
Source code in SaigeToolkit/data/transform/box_function.py
@preserve_coordinates
def zoom(
    bboxes: BBoxesType,
    image_size: Tuple[int],
    ratio: float = 1.0,
    h_start: float = 0.0,
    w_start: float = 0.0,
) -> BBoxesType:
    w, h = image_size
    if ratio == 1.0:
        return bboxes
    target_size = (int(w * ratio), int(h * ratio))
    bboxes = resize_box(bboxes, image_size, target_size[0], target_size[1])
    if ratio > 1.0:
        h_start = min(h_start, 1.0 - 1e-5)
        w_start = min(w_start, 1.0 - 1e-5)
        x1 = int((target_size[0] - w + 1) * w_start)
        y1 = int((target_size[1] - h + 1) * h_start)
        bboxes = crop_box(bboxes, [x1, y1, x1 + w, y1 + h])
    elif ratio < 1.0:
        x1 = int((w - target_size[0] + 1) * w_start)
        y1 = int((h - target_size[1] + 1) * h_start)
        bboxes = translate_box(bboxes, [-x1, -y1], image_size)

    return bboxes

random_resized_crop_and_pad

random_resized_crop_and_pad(bboxes: BBoxesType, image_size: Tuple[int], scale: float = 1.0, aspect_ratio: float = 1.0, h_start: float = 0.0, w_start: float = 0.0, height: Optional[int] = None, width: Optional[int] = None) -> BBoxesType
Source code in SaigeToolkit/data/transform/box_function.py
@preserve_coordinates
def random_resized_crop_and_pad(
    bboxes: BBoxesType,
    image_size: Tuple[int],
    scale: float = 1.0,
    aspect_ratio: float = 1.0,
    h_start: float = 0.0,
    w_start: float = 0.0,
    height: Optional[int] = None,
    width: Optional[int] = None,
) -> BBoxesType:
    w_original, h_original = image_size
    area = h_original * w_original
    target_area = scale * area

    crop_height = int(round(math.sqrt(target_area / aspect_ratio)))
    crop_width = int(round(math.sqrt(target_area * aspect_ratio)))

    w_start = min(w_start, 1.0 - 1e-5)
    h_start = min(h_start, 1.0 - 1e-5)

    # Padding
    pad_top = max(crop_height - h_original, 0)
    pad_bottom = max(crop_height - h_original, 0)
    pad_left = max(crop_width - w_original, 0)
    pad_right = max(crop_width - w_original, 0)

    padded_image_size = [w_original + pad_left + pad_right, h_original + pad_top + pad_bottom]
    bboxes = translate_box(bboxes, [-pad_left, -pad_top], padded_image_size)

    # Crop
    x1 = int((padded_image_size[0] - crop_width + 1) * w_start)
    y1 = int((padded_image_size[1] - crop_height + 1) * h_start)
    bboxes = crop_box(bboxes, [x1, y1, x1 + crop_width, y1 + crop_height])

    # Resize
    bboxes = resize_box(
        bboxes,
        [crop_width, crop_height],
        width or w_original,
        height or h_original,
    )

    return bboxes

perspective_transform

perspective_transform(bboxes: BBoxesType, image_size: Tuple[int], offset_top_left: OffsetType, offset_top_right: OffsetType, offset_bottom_right: OffsetType, offset_bottom_left: OffsetType) -> BBoxesType
Source code in SaigeToolkit/data/transform/box_function.py
@preserve_coordinates
def perspective_transform(
    bboxes: BBoxesType,
    image_size: Tuple[int],
    offset_top_left: OffsetType,
    offset_top_right: OffsetType,
    offset_bottom_right: OffsetType,
    offset_bottom_left: OffsetType,
) -> BBoxesType:
    width, height = image_size

    for point_offset in [
        offset_top_left,
        offset_top_right,
        offset_bottom_right,
        offset_bottom_left,
    ]:
        offset_x, offset_y = point_offset

        check_value(offset_x, 0, 49)
        check_value(offset_y, 0, 49)

    transform_matrix = calculate_transform_matrix(
        width=width,
        height=height,
        offset_top_left=offset_top_left,
        offset_top_right=offset_top_right,
        offset_bottom_right=offset_bottom_right,
        offset_bottom_left=offset_bottom_left,
    )

    new_bboxes = bboxes.copy()
    for idx_box, bbox in enumerate(bboxes):
        # [(x1, y1), (x2, y1), (x2, y2), (x1, y2)]
        points = [(bbox[0], bbox[1]), (bbox[2], bbox[1]), (bbox[2], bbox[3]), (bbox[0], bbox[3])]

        dtype_max_value = np.iinfo(np.int32).max
        min_x, min_y = dtype_max_value, dtype_max_value
        max_x, max_y = 0, 0

        for point in points:
            x, y, scaling_factor = np.dot(transform_matrix, np.append(point, 1))
            x = int(x / scaling_factor)
            y = int(y / scaling_factor)

            if x < min_x:
                min_x = x

            elif x > max_x:
                max_x = x

            if y < min_y:
                min_y = y

            elif y > max_y:
                max_y = y

        new_bboxes[idx_box] = [min_x, min_y, max_x, max_y]

    return new_bboxes