import abc
from typing import Type, List
import numpy as np
from gbvision.constants.types import Shape, Number, Rect, Point, Contour, Frame, Color
[docs]class BaseShape(abc.ABC):
def __init__(self):
raise Exception("Can't create an instance of a base shape class")
[docs] @staticmethod
@abc.abstractmethod
def to_bounding_rect(shape: Shape) -> Rect:
"""
Finds the bounding rect of this shape
:param shape: The shape
:return: The bounding rect of this shape, as a gbvision.Rect
"""
[docs] @staticmethod
@abc.abstractmethod
def from_bounding_rect(bounding_rect: Rect) -> Shape:
"""
Converts a rect to the closest possible object of this shape that is contained in it
:param bounding_rect: The rect to convert
:return: A shape of this type that is as close as possible to this rect
"""
[docs] @staticmethod
@abc.abstractmethod
def from_contour(cnt: Contour) -> Shape:
"""
Converts a single contour to this shape
:param cnt: The contour
:return: A shape representing this contour
"""
[docs] @classmethod
def from_contours(cls, cnts: List[Contour]) -> List[Shape]:
"""
Converts the given list of contours to a list of this shape
:param cnts: The contours to convert
:return: A list of this shape, the same length as the contours list
"""
return list(map(cls.from_contour, cnts))
[docs] @staticmethod
@abc.abstractmethod
def collision(shape1: Shape, shape2: Shape) -> bool:
"""
checks if the two shapes are colliding
:param shape1: the first shape
:param shape2: the second shape
:return: True if the shapes are colliding, False otherwise
"""
[docs] @staticmethod
@abc.abstractmethod
def area(shape: Shape) -> Number:
"""
calculates the area of the shape
:param shape: the shape
:return: the area of the shape
"""
[docs] @staticmethod
@abc.abstractmethod
def center(shape: Shape) -> Point:
"""
calculates the center-of-mass of the shape
:param shape: the shape
:return: the center of the shape
"""
[docs] @classmethod
def root_area(cls, shape: Shape) -> Number:
"""
calculates the square root of the area of the shape
default is the square root of cls.shape_area, but it can be overridden in case there is a simpler way\
(for example for circles)
:param shape: the shape
:return: the square root of the area of the shape
"""
return np.sqrt(cls.area(shape))
[docs] @classmethod
def sort(cls, shapes: List[Shape]) -> List[Shape]:
"""
sorts the list of shapes by area, should be overridden to use area root in case it's better
doesn't modify the given list, returns a new list
:param shapes: the list of shapes to sort
:return: a sorted copy of the list of shapes
"""
return sorted(shapes, key=cls.area, reverse=True)
[docs] @classmethod
def filter_inners(cls, shapes: List[Shape]) -> List[Shape]:
"""
filters out all shapes that are colliding with a shape with a smaller index in the given list
returns a list of all shapes that aren't colliding
:param shapes: the list of shapes
:return: the filtered list of shapes
"""
filtered_shapes = []
for i, shape in enumerate(shapes):
shape_invalid = False
for j in range(i):
shape_invalid = cls.collision(shape, shapes[j])
if shape_invalid:
break
if not shape_invalid:
filtered_shapes.append(shape)
return filtered_shapes
[docs] @classmethod
def from_contours_sorted(cls, cnts: List[Contour]) -> List[Shape]:
"""
Converts the given list of contours to a list of this shape, and sorts them by size
:param cnts: The contours to convert
:return: A list of this shape, the same length as the contours list
"""
return cls.sort(cls.from_contours(cnts))
[docs] @classmethod
def distance_squared(cls, shape1: Shape, shape2: Shape) -> Number:
"""
Finds the square of the distance between the centers of the shapes
:param shape1: The first shape
:param shape2: The second shape
:return: The square of distance between the two centers
"""
center1 = cls.center(shape1)
center2 = cls.center(shape2)
return (center1[0] - center2[0]) ** 2 + (center1[1] - center2[1]) ** 2
[docs] @classmethod
def distance(cls, shape1: Shape, shape2: Shape) -> Number:
"""
Finds the square of the distance between the centers of the shapes
:param shape1: The first shape
:param shape2: The second shape
:return: The square of distance between the two centers
"""
return np.sqrt(cls.distance_squared(shape1, shape2))
@staticmethod
@abc.abstractmethod
def _unsafe_draw(frame: Frame, shape: Shape, color: Color, *args, **kwargs) -> None:
"""
Unsafely draws this shape on the frame
This method should modify the given frame and not return anything
:param frame: The frame to draw on
:param shape: The shape to draw
:param color: The color to draw with
:param args: Optional arguments to pass to the drawing function
:param kwargs: Optional keyword arguments to pass to the drawing function
"""
@classmethod
def _unsafe_draw_multiple(cls, frame: Frame, shapes: List[Shape], color: Color, *args, **kwargs) -> None:
"""
Unsafely draws this object on the frame
This method should modify the given frame and not return anything
:param frame: The frame to draw on
:param shapes: The list of shapes to draw
:param color: The color to draw with
:param args: Optional arguments to pass to the drawing function
:param kwargs: Optional keyword arguments to pass to the drawing function
"""
for shape in shapes:
cls._unsafe_draw(frame, shape, color, *args, **kwargs)
[docs] @classmethod
def draw(cls, frame: Frame, shape: Shape, color: Color, *args, **kwargs) -> Frame:
"""
Draws the given shape on the frame
This method does not change to original frame, and instead creates a copy of it and returns the copy
:param frame: The frame to draw on
:param shape: The shape to draw
:param color: The color to use
:param args: Optional arguments to pass to the drawing function
:param kwargs: Optional keyword arguments to pass to the drawing function
:return: A copy of the frame with the shape drawn on it
"""
frame = frame.copy()
cls._unsafe_draw(frame, shape, color, *args, **kwargs)
return frame
[docs] @staticmethod
@abc.abstractmethod
def set_center(shape: Shape, new_center: Point) -> Shape:
"""
Returns an identical copy that has been moved to a new location
Only the center should be different
:param shape: The shape
:param new_center: The new center, calling BaseShape.center on the return value should result in this
:return: A copy of this shape, with the new center
"""
[docs] @classmethod
def draw_multiple(cls, frame: Frame, shapes: List[Shape], color: Color, *args, **kwargs) -> Frame:
"""
Draws the given shapes on the frame
This method does not change to original frame, and instead creates a copy of it and returns the copy
:param frame: The frame to draw on
:param shapes: The list of shapes to draw
:param color: The color to use
:param args: Optional arguments to pass to the drawing function
:param kwargs: Optional keyword arguments to pass to the drawing function
:return: A copy of the frame with the shape drawn on it
"""
frame = frame.copy()
cls._unsafe_draw_multiple(frame, shapes, color, *args, **kwargs)
return frame
[docs] @classmethod
def sort_and_filter_inners(cls, shapes: List[Shape]):
"""
Sorts the given list of shapes and filters out any inner shapes
:param shapes: The list of shapes
:return: The list of shapes sorted, with all inner shapes filtered out
"""
return cls.filter_inners(cls.sort(shapes))
BaseShapeType = Type[BaseShape]