287 lines
10 KiB
Python
287 lines
10 KiB
Python
# See https://stackoverflow.com/a/39757388
|
|
from __future__ import annotations
|
|
|
|
from functools import lru_cache
|
|
from operator import attrgetter
|
|
from typing import TYPE_CHECKING, Iterable
|
|
|
|
from funcy import juxt, rpartial, compose, lflatten, first, second
|
|
|
|
from cv_analysis.utils import lift
|
|
|
|
if TYPE_CHECKING:
|
|
from cv_analysis.utils.rectangle import Rectangle
|
|
|
|
|
|
def adjacent(alpha: Rectangle, beta: Rectangle, tolerance=7, strict=False):
|
|
"""Checks if the two rectangles are adjacent to each other.
|
|
|
|
Args:
|
|
alpha: The first rectangle.
|
|
beta: The second rectangle.
|
|
tolerance: The maximum distance between the two rectangles.
|
|
strict: If True, the rectangles must be adjacent along one axis and contained within the other axis. Else, the
|
|
rectangles must be adjacent along one axis and overlapping the other axis.
|
|
Returns:
|
|
True if the two rectangles are adjacent to each other, False otherwise.
|
|
"""
|
|
select_strictness_variant = first if strict else second
|
|
test_candidates = [
|
|
# +---+
|
|
# | | +---+
|
|
# | a | | b |
|
|
# | | +___+
|
|
# +___+
|
|
(right_left_aligned_and_vertically_contained, right_left_aligned_and_vertically_overlapping),
|
|
# +---+
|
|
# +---+ | |
|
|
# | b | | a |
|
|
# +___+ | |
|
|
# +___+
|
|
(left_right_aligned_and_vertically_contained, left_right_aligned_and_vertically_overlapping),
|
|
# +-----------+
|
|
# | a |
|
|
# +___________+
|
|
# +-----+
|
|
# | b |
|
|
# +_____+
|
|
(bottom_top_aligned_and_horizontally_contained, bottom_top_aligned_and_horizontally_overlapping),
|
|
# +-----+
|
|
# | b |
|
|
# +_____+
|
|
# +-----------+
|
|
# | a |
|
|
# +___________+
|
|
(top_bottom_aligned_and_horizontally_contained, top_bottom_aligned_and_horizontally_overlapping),
|
|
]
|
|
|
|
tests = map(select_strictness_variant, test_candidates)
|
|
return any(juxt(*tests)(alpha, beta, tolerance))
|
|
|
|
|
|
def right_left_aligned_and_vertically_overlapping(alpha: Rectangle, beta: Rectangle, tol):
|
|
"""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):
|
|
"""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):
|
|
"""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):
|
|
"""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
|
|
)
|
|
|
|
|
|
def right_left_aligned_and_vertically_contained(alpha: Rectangle, beta: Rectangle, tol):
|
|
"""Checks if the first rectangle is left of the other within a tolerance and also contains the other's y range."""
|
|
return adjacent_along_one_axis_and_contained_within_perpendicular_axis(
|
|
alpha.x2, beta.x1, beta.y1, beta.y2, alpha.y1, alpha.y2, tolerance=tol
|
|
)
|
|
|
|
|
|
def left_right_aligned_and_vertically_contained(alpha: Rectangle, beta: Rectangle, tol):
|
|
"""Checks if the first rectangle is right of the other within a tolerance and also contains the other's y range."""
|
|
return adjacent_along_one_axis_and_contained_within_perpendicular_axis(
|
|
alpha.x1, beta.x2, beta.y1, beta.y2, alpha.y1, alpha.y2, tolerance=tol
|
|
)
|
|
|
|
|
|
def bottom_top_aligned_and_horizontally_contained(alpha: Rectangle, beta: Rectangle, tol):
|
|
"""Checks if the first rectangle is above the other within a tolerance and also contains the other's x range."""
|
|
return adjacent_along_one_axis_and_contained_within_perpendicular_axis(
|
|
alpha.y2, beta.y1, beta.x1, beta.x2, alpha.x1, alpha.x2, tolerance=tol
|
|
)
|
|
|
|
|
|
def top_bottom_aligned_and_horizontally_contained(alpha: Rectangle, beta: Rectangle, tol):
|
|
"""Checks if the first rectangle is below the other within a tolerance and also contains the other's x range."""
|
|
return adjacent_along_one_axis_and_contained_within_perpendicular_axis(
|
|
alpha.y1, beta.y2, beta.x1, beta.x2, alpha.x1, alpha.x2, tolerance=tol
|
|
)
|
|
|
|
|
|
def adjacent_along_one_axis_and_overlapping_along_perpendicular_axis(
|
|
axis_0_point_1,
|
|
axis_1_point_2,
|
|
axis_1_contained_point_1,
|
|
axis_1_contained_point_2,
|
|
axis_1_lower_bound,
|
|
axis_1_upper_bound,
|
|
tolerance,
|
|
):
|
|
"""Checks if two points are adjacent along one axis and two other points overlap a range along the perpendicular
|
|
axis.
|
|
"""
|
|
return adjacent_along_one_axis_and_overlapping_or_contained_along_perpendicular_axis(
|
|
axis_0_point_1,
|
|
axis_1_point_2,
|
|
axis_1_contained_point_1,
|
|
axis_1_contained_point_2,
|
|
axis_1_lower_bound,
|
|
axis_1_upper_bound,
|
|
tolerance,
|
|
mode="overlapping",
|
|
)
|
|
|
|
|
|
def adjacent_along_one_axis_and_contained_within_perpendicular_axis(
|
|
axis_0_point_1,
|
|
axis_1_point_2,
|
|
axis_1_contained_point_1,
|
|
axis_1_contained_point_2,
|
|
axis_1_lower_bound,
|
|
axis_1_upper_bound,
|
|
tolerance,
|
|
):
|
|
"""Checks if two points are adjacent along one axis and two other points overlap a range along the perpendicular
|
|
axis.
|
|
"""
|
|
return adjacent_along_one_axis_and_overlapping_or_contained_along_perpendicular_axis(
|
|
axis_0_point_1,
|
|
axis_1_point_2,
|
|
axis_1_contained_point_1,
|
|
axis_1_contained_point_2,
|
|
axis_1_lower_bound,
|
|
axis_1_upper_bound,
|
|
tolerance,
|
|
mode="contained",
|
|
)
|
|
|
|
|
|
def adjacent_along_one_axis_and_overlapping_or_contained_along_perpendicular_axis(
|
|
axis_0_point_1,
|
|
axis_1_point_2,
|
|
axis_1_contained_point_1,
|
|
axis_1_contained_point_2,
|
|
axis_1_lower_bound,
|
|
axis_1_upper_bound,
|
|
tolerance,
|
|
mode,
|
|
):
|
|
"""Checks if two points are adjacent along one axis and two other points overlap a range along the perpendicular
|
|
axis or are contained in that range, depending on the mode specified.
|
|
"""
|
|
assert mode in ["overlapping", "contained"]
|
|
quantifier = any if mode == "overlapping" else all
|
|
return all(
|
|
[
|
|
abs(axis_0_point_1 - axis_1_point_2) <= tolerance,
|
|
quantifier(
|
|
[
|
|
axis_1_lower_bound <= p <= axis_1_upper_bound
|
|
for p in [axis_1_contained_point_1, axis_1_contained_point_2]
|
|
]
|
|
),
|
|
]
|
|
)
|
|
|
|
|
|
def contains(alpha: Rectangle, beta: Rectangle, tol=3):
|
|
"""Checks if the first rectangle contains the second rectangle."""
|
|
return (
|
|
beta.x1 + tol >= alpha.x1
|
|
and beta.y1 + tol >= alpha.y1
|
|
and beta.x2 - tol <= alpha.x2
|
|
and beta.y2 - tol <= alpha.y2
|
|
)
|
|
|
|
|
|
def is_contained(rectangle: Rectangle, rectangles: Iterable[Rectangle]):
|
|
"""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 iou(alpha: Rectangle, beta: Rectangle):
|
|
"""Calculates the intersection area over the union area of two rectangles."""
|
|
return intersection(alpha, beta) / union(alpha, beta)
|
|
|
|
|
|
def area(rectangle: Rectangle):
|
|
"""Calculates the area of a rectangle."""
|
|
return abs((rectangle.x2 - rectangle.x1) * (rectangle.y2 - rectangle.y1))
|
|
|
|
|
|
def union(alpha: Rectangle, beta: Rectangle):
|
|
"""Calculates the union area of two rectangles."""
|
|
return area(alpha) + area(beta) - intersection(alpha, beta)
|
|
|
|
|
|
@lru_cache(maxsize=1000)
|
|
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)))
|
|
|
|
sort_first_components, sort_second_components = map(make_access_components_and_sort_fn, [1, 2])
|
|
|
|
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
|
|
|
|
|
|
def related(alpha: Rectangle, beta: Rectangle):
|
|
return close(alpha, beta) or overlap(alpha, beta)
|
|
|
|
|
|
def close(alpha: Rectangle, beta: Rectangle, max_gap=14):
|
|
# FIXME: Parameterize via factory
|
|
return adjacent(alpha, beta, tolerance=max_gap, strict=True)
|
|
|
|
|
|
def overlap(alpha: Rectangle, beta: Rectangle):
|
|
return intersection(alpha, beta) > 0
|