diff --git a/cv_analysis/utils/spacial.py b/cv_analysis/utils/spacial.py index fd51e40..2787ed7 100644 --- a/cv_analysis/utils/spacial.py +++ b/cv_analysis/utils/spacial.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: def adjacent(alpha: Rectangle, beta: Rectangle, tolerance=7): - """Check if the two rectangles are adjacent to each other.""" + """Checks if the two rectangles are adjacent to each other.""" return any( juxt( # +---+ @@ -47,28 +47,28 @@ def adjacent(alpha: Rectangle, beta: Rectangle, tolerance=7): def right_left_aligned_and_vertically_overlapping(alpha: Rectangle, beta: Rectangle, tol): - """Check if the first rectangle is left of the other within a tolerance and also overlaps the other's y range.""" + """Checks if the first rectangle is left of the other within a tolerance and also overlaps the other's y range.""" return adjacent_along_one_axis_and_overlapping_along_perpendicular_axis( alpha.x2, beta.x1, beta.y1, beta.y2, alpha.y1, alpha.y2, tolerance=tol ) def left_right_aligned_and_vertically_overlapping(alpha: Rectangle, beta: Rectangle, tol): - """Check if the first rectangle is right of the other within a tolerance and also overlaps the other's y range.""" + """Checks if the first rectangle is right of the other within a tolerance and also overlaps the other's y range.""" return adjacent_along_one_axis_and_overlapping_along_perpendicular_axis( alpha.x1, beta.x2, beta.y1, beta.y2, alpha.y1, alpha.y2, tolerance=tol ) def bottom_top_aligned_and_horizontally_overlapping(alpha: Rectangle, beta: Rectangle, tol): - """Check if the first rectangle is above the other within a tolerance and also overlaps the other's x range.""" + """Checks if the first rectangle is above the other within a tolerance and also overlaps the other's x range.""" return adjacent_along_one_axis_and_overlapping_along_perpendicular_axis( alpha.y2, beta.y1, beta.x1, beta.x2, alpha.x1, alpha.x2, tolerance=tol ) def top_bottom_aligned_and_horizontally_overlapping(alpha: Rectangle, beta: Rectangle, tol): - """Check if the first rectangle is below the other within a tolerance and also overlaps the other's x range.""" + """Checks if the first rectangle is below the other within a tolerance and also overlaps the other's x range.""" return adjacent_along_one_axis_and_overlapping_along_perpendicular_axis( alpha.y1, beta.y2, beta.x1, beta.x2, alpha.x1, alpha.x2, tolerance=tol ) @@ -83,7 +83,7 @@ def adjacent_along_one_axis_and_overlapping_along_perpendicular_axis( axis_1_upper_bound, tolerance, ): - """Check if two points are adjacent along one axis and two other points overlap a range along the perpendicular + """Checks if two points are adjacent along one axis and two other points overlap a range along the perpendicular axis.""" return all( [ @@ -99,7 +99,7 @@ def adjacent_along_one_axis_and_overlapping_along_perpendicular_axis( def contains(alpha: Rectangle, beta: Rectangle, tol=3): - """Check if the first rectangle contains the second rectangle.""" + """Checks if the first rectangle contains the second rectangle.""" return ( beta.x1 + tol >= alpha.x1 and beta.y1 + tol >= alpha.y1 @@ -109,49 +109,56 @@ def contains(alpha: Rectangle, beta: Rectangle, tol=3): def is_contained(rectangle: Rectangle, rectangles: Iterable[Rectangle]): - """Check if the rectangle is contained within any of the other rectangles.""" + """Checks if the rectangle is contained within any of the other rectangles.""" other_rectangles = filter(lambda r: r != rectangle, rectangles) return any(map(rpartial(contains, rectangle), other_rectangles)) def intersection(alpha, beta): + """Calculates the intersection of two rectangles.""" return intersection_along_x_axis(alpha, beta) * intersection_along_y_axis(alpha, beta) def intersection_along_x_axis(alpha, beta): + """Calculates the intersection along the x-axis.""" return intersection_along_axis(alpha, beta, "x") def intersection_along_y_axis(alpha, beta): + """Calculates the intersection along the y-axis.""" return intersection_along_axis(alpha, beta, "y") def intersection_along_axis(alpha, beta, axis): + """Calculates the intersection along the given axis. + + Cases: + a b + [-----] (---) ==> [a1, b1, a2, b2] ==> max(0, (a2 - b1)) = 0 + b a + (---) [-----] ==> [b1, a1, b2, a2] ==> max(0, (b2 - a1)) = 0 + a b + [--(----]----) ==> [a1, b1, a2, b2] ==> max(0, (a2 - b1)) = (a2 - b1) + a b + (-[---]----) ==> [b1, a1, a2, b2] ==> max(0, (a2 - a1)) = (a2 - a1) + b a + [-(---)----] ==> [a1, b1, b2, a2] ==> max(0, (b2 - b1)) = (b2 - b1) + b a + (----[--)----] ==> [b1, a1, b2, a2] ==> max(0, (b2 - a1)) = (b2 - a1) + """ assert axis in ["x", "y"] def get_component_accessor(component): + """Returns a function that accesses the given component of a rectangle.""" return attrgetter(f"{axis}{component}") def make_access_components_and_sort_fn(component): + """Returns a function that accesses and sorts the given component of multiple rectangles.""" + assert component in [1, 2] return compose(sorted, lift(get_component_accessor(component))) - c1 = make_access_components_and_sort_fn(1) - c2 = make_access_components_and_sort_fn(2) + sort_first_components, sort_second_components = map(make_access_components_and_sort_fn, [1, 2]) - # Cases: - # a b - # [-----] (---) => [a1, b1, a2, b2] => max(0, (a2 - b1)) = 0 - # b a - # (---) [-----] => [b1, a1, b2, a2] => max(0, (b2 - a1)) = 0 - # a b - # [--(----]----) => [a1, b1, a2, b2] => max(0, (a2 - b1)) = (a2 - b1) - # a b - # (-[---]----) => [b1, a1, a2, b2] => max(0, (a2 - a1)) = (a2 - a1) - # b a - # [-(---)----] => [a1, b1, b2, a2] => max(0, (b2 - b1)) = (b2 - b1) - # b a - # (----[--)----] => [b1, a1, b2, a2] => max(0, (b2 - a1)) = (b2 - a1) - - coords = lflatten(juxt(c1, c2)((alpha, beta))) - intersection = max(0, coords[2] - coords[1]) + min_c1, max_c1, min_c2, max_c2 = lflatten(juxt(sort_first_components, sort_second_components)((alpha, beta))) + intersection = max(0, min_c2 - max_c1) return intersection