Skip to content

Geometric

Geometry Transformer component

GeometrizerImage

GeometrizerImage class

Source code in otary/image/components/transformer/components/geometrizer/geometrizer.py
class GeometrizerImage:
    """GeometrizerImage class"""

    def __init__(self, base: BaseImage) -> None:
        self.base = base

    def shift(self, shift: NDArray, fill_value: Sequence[float] = (0.0,)) -> None:
        """Shift the image by performing a translation operation

        Args:
            shift (NDArray): Vector for translation
            fill_value (int | tuple[int, int, int], optional): value to fill the
                border of the image after the rotation in case reshape is True.
                Can be a tuple of 3 integers for RGB image or a single integer for
                grayscale image. Defaults to (0.0,) which is black.
        """
        vector_shift = assert_transform_shift_vector(vector=shift)
        shift_matrix = np.asarray(
            [[1.0, 0.0, vector_shift[0]], [0.0, 1.0, vector_shift[1]]],
            dtype=np.float32,
        )

        self.base.asarray = cv2.warpAffine(
            src=self.base.asarray,
            M=shift_matrix,
            dsize=(self.base.width, self.base.height),
            flags=cv2.INTER_LINEAR,
            borderMode=cv2.BORDER_CONSTANT,
            borderValue=fill_value,
        )  # type: ignore[call-overload]

    def __rotate_exact(
        self,
        angle: float,
        is_degree: bool = False,
        is_clockwise: bool = True,
        reshape: bool = True,
        border_fill_value: float = 0.0,
    ) -> None:
        """Rotate the image by a given angle.
        This method is more accurate than the rotate method but way slower
        (about 10 times slower).

        Args:
            angle (float): angle to rotate the image
            is_degree (bool, optional): whether the angle is in degree or not.
                If not it is considered to be in radians.
                Defaults to False which means radians.
            is_clockwise (bool, optional): whether the rotation is clockwise or
                counter-clockwise. Defaults to True.
            reshape (bool, optional): scipy reshape option. Defaults to True.
            border_fill_value (float, optional): value to fill the border of the image
                after the rotation in case reshape is True. Can only be a single
                integer. Does not support tuple of 3 integers for RGB image.
                Defaults to 0.0 which is black.
        """
        # pylint: disable=too-many-arguments, too-many-positional-arguments
        if not is_degree:
            angle = np.rad2deg(angle)
        if is_clockwise:
            # by default scipy rotate is counter-clockwise
            angle = -angle
        self.base.asarray = scipy.ndimage.rotate(
            input=self.base.asarray,
            angle=angle,
            reshape=reshape,
            cval=border_fill_value,
        )

    def rotate(
        self,
        angle: float,
        is_degree: bool = False,
        is_clockwise: bool = True,
        reshape: bool = True,
        fill_value: Sequence[float] = (0.0,),
        fast: bool = True,
    ) -> None:
        """Rotate the image by a given angle.

        For the rotation with reshape, meaning preserving the whole image,
        we used the code from the imutils library:
        https://github.com/PyImageSearch/imutils/blob/master/imutils/convenience.py#L41

        Args:
            angle (float): angle to rotate the image
            is_degree (bool, optional): whether the angle is in degree or not.
                If not it is considered to be in radians.
                Defaults to False which means radians.
            is_clockwise (bool, optional): whether the rotation is clockwise or
                counter-clockwise. Defaults to True.
            reshape (bool, optional): whether to preserve the original image or not.
                If True, the complete image is preserved hence the width and height
                of the rotated image are different than in the original image.
                Defaults to True.
            fill_value (Sequence[float], optional): value to
                fill the border of the image after the rotation in case reshape is True.
                Can be a tuple of 3 integers for RGB image or a single integer for
                grayscale image. Defaults to (0.0,) which is black.
        """
        # pylint: disable=too-many-arguments, too-many-positional-arguments
        # pylint: disable=too-many-locals
        if not fast:  # using scipy rotate which is slower than cv2
            fill_value_scalar = fill_value[0]
            if not isinstance(fill_value_scalar, float):
                raise ValueError(
                    f"The fill_value {fill_value_scalar} is not a valid "
                    "value. It must be a single integer when fast mode is off"
                )
            self.__rotate_exact(
                angle=angle,
                is_degree=is_degree,
                is_clockwise=is_clockwise,
                reshape=reshape,
                border_fill_value=fill_value_scalar,
            )
            return None

        if not is_degree:
            angle = np.rad2deg(angle)
        if is_clockwise:
            angle = -angle

        h, w = self.base.asarray.shape[:2]
        center = (w / 2, h / 2)

        # Compute rotation matrix
        rotmat = cv2.getRotationMatrix2D(center, angle, 1.0)  # param angle in degree

        if reshape:
            # Compute new bounding dimensions
            cos_a = np.abs(rotmat[0, 0])
            sin_a = np.abs(rotmat[0, 1])
            new_w = int((h * sin_a) + (w * cos_a))
            new_h = int((h * cos_a) + (w * sin_a))
            w, h = new_w, new_h

            # Adjust the rotation matrix to shift the image center
            rotmat[0, 2] += (w / 2) - center[0]
            rotmat[1, 2] += (h / 2) - center[1]

        self.base.asarray = cv2.warpAffine(
            src=self.base.asarray,
            M=rotmat,
            dsize=(w, h),
            flags=cv2.INTER_LINEAR,
            borderMode=cv2.BORDER_CONSTANT,
            borderValue=fill_value,
        )  # type: ignore[call-overload]
        return None

    def center_to_point(self, point: NDArray) -> NDArray:
        """Shift the image so that the input point ends up in the middle of the
        new image

        Args:
            point (NDArray): point as (2,) shape numpy array

        Returns:
            NDArray: translation Vector
        """
        shift_vector = self.base.center - point
        self.shift(shift=shift_vector)
        return shift_vector

    def center_to_segment(self, segment: NDArray) -> NDArray:
        """Shift the image so that the segment middle point ends up in the middle
        of the new image

        Args:
            segment (NDArray): segment as numpy array of shape (2, 2)

        Returns:
            NDArray: vector_shift
        """
        return self.center_to_point(point=geo.Segment(segment).centroid)

    def restrict_rect_in_frame(self, rectangle: geo.Rectangle) -> geo.Rectangle:
        """Create a new rectangle that is contained within the image borders.
        If the input rectangle is outside the image, the returned rectangle is a
        image frame-fitted rectangle that preserve the same shape.

        Args:
            rectangle (geo.Rectangle): input rectangle

        Returns:
            geo.Rectangle: new rectangle
        """
        # rectangle boundaries
        xmin, xmax = rectangle.xmin, rectangle.xmax
        ymin, ymax = rectangle.ymin, rectangle.ymax

        # recalculate boundaries based on image shape
        xmin = max(0, xmin)
        ymin = max(0, ymin)
        xmax = min(self.base.width, xmax)
        ymax = min(self.base.height, ymax)

        # recreate a rectangle with new coordinates
        rect_restricted = geo.Rectangle.from_topleft_bottomright(
            topleft=np.asarray([xmin, ymin]),
            bottomright=np.asarray([xmax, ymax]),
            is_cast_int=True,
        )
        return rect_restricted

__rotate_exact(angle, is_degree=False, is_clockwise=True, reshape=True, border_fill_value=0.0)

Rotate the image by a given angle. This method is more accurate than the rotate method but way slower (about 10 times slower).

Parameters:

Name Type Description Default
angle float

angle to rotate the image

required
is_degree bool

whether the angle is in degree or not. If not it is considered to be in radians. Defaults to False which means radians.

False
is_clockwise bool

whether the rotation is clockwise or counter-clockwise. Defaults to True.

True
reshape bool

scipy reshape option. Defaults to True.

True
border_fill_value float

value to fill the border of the image after the rotation in case reshape is True. Can only be a single integer. Does not support tuple of 3 integers for RGB image. Defaults to 0.0 which is black.

0.0
Source code in otary/image/components/transformer/components/geometrizer/geometrizer.py
def __rotate_exact(
    self,
    angle: float,
    is_degree: bool = False,
    is_clockwise: bool = True,
    reshape: bool = True,
    border_fill_value: float = 0.0,
) -> None:
    """Rotate the image by a given angle.
    This method is more accurate than the rotate method but way slower
    (about 10 times slower).

    Args:
        angle (float): angle to rotate the image
        is_degree (bool, optional): whether the angle is in degree or not.
            If not it is considered to be in radians.
            Defaults to False which means radians.
        is_clockwise (bool, optional): whether the rotation is clockwise or
            counter-clockwise. Defaults to True.
        reshape (bool, optional): scipy reshape option. Defaults to True.
        border_fill_value (float, optional): value to fill the border of the image
            after the rotation in case reshape is True. Can only be a single
            integer. Does not support tuple of 3 integers for RGB image.
            Defaults to 0.0 which is black.
    """
    # pylint: disable=too-many-arguments, too-many-positional-arguments
    if not is_degree:
        angle = np.rad2deg(angle)
    if is_clockwise:
        # by default scipy rotate is counter-clockwise
        angle = -angle
    self.base.asarray = scipy.ndimage.rotate(
        input=self.base.asarray,
        angle=angle,
        reshape=reshape,
        cval=border_fill_value,
    )

center_to_point(point)

Shift the image so that the input point ends up in the middle of the new image

Parameters:

Name Type Description Default
point NDArray

point as (2,) shape numpy array

required

Returns:

Name Type Description
NDArray NDArray

translation Vector

Source code in otary/image/components/transformer/components/geometrizer/geometrizer.py
def center_to_point(self, point: NDArray) -> NDArray:
    """Shift the image so that the input point ends up in the middle of the
    new image

    Args:
        point (NDArray): point as (2,) shape numpy array

    Returns:
        NDArray: translation Vector
    """
    shift_vector = self.base.center - point
    self.shift(shift=shift_vector)
    return shift_vector

center_to_segment(segment)

Shift the image so that the segment middle point ends up in the middle of the new image

Parameters:

Name Type Description Default
segment NDArray

segment as numpy array of shape (2, 2)

required

Returns:

Name Type Description
NDArray NDArray

vector_shift

Source code in otary/image/components/transformer/components/geometrizer/geometrizer.py
def center_to_segment(self, segment: NDArray) -> NDArray:
    """Shift the image so that the segment middle point ends up in the middle
    of the new image

    Args:
        segment (NDArray): segment as numpy array of shape (2, 2)

    Returns:
        NDArray: vector_shift
    """
    return self.center_to_point(point=geo.Segment(segment).centroid)

restrict_rect_in_frame(rectangle)

Create a new rectangle that is contained within the image borders. If the input rectangle is outside the image, the returned rectangle is a image frame-fitted rectangle that preserve the same shape.

Parameters:

Name Type Description Default
rectangle Rectangle

input rectangle

required

Returns:

Type Description
Rectangle

geo.Rectangle: new rectangle

Source code in otary/image/components/transformer/components/geometrizer/geometrizer.py
def restrict_rect_in_frame(self, rectangle: geo.Rectangle) -> geo.Rectangle:
    """Create a new rectangle that is contained within the image borders.
    If the input rectangle is outside the image, the returned rectangle is a
    image frame-fitted rectangle that preserve the same shape.

    Args:
        rectangle (geo.Rectangle): input rectangle

    Returns:
        geo.Rectangle: new rectangle
    """
    # rectangle boundaries
    xmin, xmax = rectangle.xmin, rectangle.xmax
    ymin, ymax = rectangle.ymin, rectangle.ymax

    # recalculate boundaries based on image shape
    xmin = max(0, xmin)
    ymin = max(0, ymin)
    xmax = min(self.base.width, xmax)
    ymax = min(self.base.height, ymax)

    # recreate a rectangle with new coordinates
    rect_restricted = geo.Rectangle.from_topleft_bottomright(
        topleft=np.asarray([xmin, ymin]),
        bottomright=np.asarray([xmax, ymax]),
        is_cast_int=True,
    )
    return rect_restricted

rotate(angle, is_degree=False, is_clockwise=True, reshape=True, fill_value=(0.0,), fast=True)

Rotate the image by a given angle.

For the rotation with reshape, meaning preserving the whole image, we used the code from the imutils library: https://github.com/PyImageSearch/imutils/blob/master/imutils/convenience.py#L41

Parameters:

Name Type Description Default
angle float

angle to rotate the image

required
is_degree bool

whether the angle is in degree or not. If not it is considered to be in radians. Defaults to False which means radians.

False
is_clockwise bool

whether the rotation is clockwise or counter-clockwise. Defaults to True.

True
reshape bool

whether to preserve the original image or not. If True, the complete image is preserved hence the width and height of the rotated image are different than in the original image. Defaults to True.

True
fill_value Sequence[float]

value to fill the border of the image after the rotation in case reshape is True. Can be a tuple of 3 integers for RGB image or a single integer for grayscale image. Defaults to (0.0,) which is black.

(0.0,)
Source code in otary/image/components/transformer/components/geometrizer/geometrizer.py
def rotate(
    self,
    angle: float,
    is_degree: bool = False,
    is_clockwise: bool = True,
    reshape: bool = True,
    fill_value: Sequence[float] = (0.0,),
    fast: bool = True,
) -> None:
    """Rotate the image by a given angle.

    For the rotation with reshape, meaning preserving the whole image,
    we used the code from the imutils library:
    https://github.com/PyImageSearch/imutils/blob/master/imutils/convenience.py#L41

    Args:
        angle (float): angle to rotate the image
        is_degree (bool, optional): whether the angle is in degree or not.
            If not it is considered to be in radians.
            Defaults to False which means radians.
        is_clockwise (bool, optional): whether the rotation is clockwise or
            counter-clockwise. Defaults to True.
        reshape (bool, optional): whether to preserve the original image or not.
            If True, the complete image is preserved hence the width and height
            of the rotated image are different than in the original image.
            Defaults to True.
        fill_value (Sequence[float], optional): value to
            fill the border of the image after the rotation in case reshape is True.
            Can be a tuple of 3 integers for RGB image or a single integer for
            grayscale image. Defaults to (0.0,) which is black.
    """
    # pylint: disable=too-many-arguments, too-many-positional-arguments
    # pylint: disable=too-many-locals
    if not fast:  # using scipy rotate which is slower than cv2
        fill_value_scalar = fill_value[0]
        if not isinstance(fill_value_scalar, float):
            raise ValueError(
                f"The fill_value {fill_value_scalar} is not a valid "
                "value. It must be a single integer when fast mode is off"
            )
        self.__rotate_exact(
            angle=angle,
            is_degree=is_degree,
            is_clockwise=is_clockwise,
            reshape=reshape,
            border_fill_value=fill_value_scalar,
        )
        return None

    if not is_degree:
        angle = np.rad2deg(angle)
    if is_clockwise:
        angle = -angle

    h, w = self.base.asarray.shape[:2]
    center = (w / 2, h / 2)

    # Compute rotation matrix
    rotmat = cv2.getRotationMatrix2D(center, angle, 1.0)  # param angle in degree

    if reshape:
        # Compute new bounding dimensions
        cos_a = np.abs(rotmat[0, 0])
        sin_a = np.abs(rotmat[0, 1])
        new_w = int((h * sin_a) + (w * cos_a))
        new_h = int((h * cos_a) + (w * sin_a))
        w, h = new_w, new_h

        # Adjust the rotation matrix to shift the image center
        rotmat[0, 2] += (w / 2) - center[0]
        rotmat[1, 2] += (h / 2) - center[1]

    self.base.asarray = cv2.warpAffine(
        src=self.base.asarray,
        M=rotmat,
        dsize=(w, h),
        flags=cv2.INTER_LINEAR,
        borderMode=cv2.BORDER_CONSTANT,
        borderValue=fill_value,
    )  # type: ignore[call-overload]
    return None

shift(shift, fill_value=(0.0,))

Shift the image by performing a translation operation

Parameters:

Name Type Description Default
shift NDArray

Vector for translation

required
fill_value int | tuple[int, int, int]

value to fill the border of the image after the rotation in case reshape is True. Can be a tuple of 3 integers for RGB image or a single integer for grayscale image. Defaults to (0.0,) which is black.

(0.0,)
Source code in otary/image/components/transformer/components/geometrizer/geometrizer.py
def shift(self, shift: NDArray, fill_value: Sequence[float] = (0.0,)) -> None:
    """Shift the image by performing a translation operation

    Args:
        shift (NDArray): Vector for translation
        fill_value (int | tuple[int, int, int], optional): value to fill the
            border of the image after the rotation in case reshape is True.
            Can be a tuple of 3 integers for RGB image or a single integer for
            grayscale image. Defaults to (0.0,) which is black.
    """
    vector_shift = assert_transform_shift_vector(vector=shift)
    shift_matrix = np.asarray(
        [[1.0, 0.0, vector_shift[0]], [0.0, 1.0, vector_shift[1]]],
        dtype=np.float32,
    )

    self.base.asarray = cv2.warpAffine(
        src=self.base.asarray,
        M=shift_matrix,
        dsize=(self.base.width, self.base.height),
        flags=cv2.INTER_LINEAR,
        borderMode=cv2.BORDER_CONSTANT,
        borderValue=fill_value,
    )  # type: ignore[call-overload]