55 lines
1.8 KiB
Python
55 lines
1.8 KiB
Python
from functools import reduce
|
|
from itertools import combinations
|
|
from typing import List, Tuple, Set
|
|
|
|
from funcy import all
|
|
|
|
from cv_analysis.utils import until, make_merger_sentinel
|
|
from cv_analysis.utils.rectangle import Rectangle
|
|
from cv_analysis.utils.spacial import related
|
|
|
|
|
|
def merge_related_rectangles(rectangles: List[Rectangle]) -> List[Rectangle]:
|
|
"""Merges rectangles that are related to each other, iterating on partial merge results until no more mergers are
|
|
possible."""
|
|
assert isinstance(rectangles, list)
|
|
no_new_merges = make_merger_sentinel()
|
|
return until(no_new_merges, merge_rectangles_once, rectangles)
|
|
|
|
|
|
def merge_rectangles_once(rectangles: List[Rectangle]) -> List[Rectangle]:
|
|
"""Merges rectangles that are related to each other, but does not iterate on the results."""
|
|
rectangles = set(rectangles)
|
|
merged, used = reduce(merge_if_related, combinations(rectangles, 2), (set(), set()))
|
|
|
|
return list(merged | rectangles - used)
|
|
|
|
|
|
T = Tuple[Set[Rectangle], Set[Rectangle]]
|
|
V = Tuple[Rectangle, Rectangle]
|
|
|
|
|
|
def merge_if_related(merged_and_used_so_far: T, rectangle_pair: V) -> T:
|
|
"""Merges two rectangles if they are related, otherwise returns the accumulator unchanged."""
|
|
alpha, beta = rectangle_pair
|
|
merged, used = merged_and_used_so_far
|
|
|
|
def unused(*args) -> bool:
|
|
return not used & {*args}
|
|
|
|
if all(unused, (alpha, beta)) and related(alpha, beta):
|
|
return merged | {bounding_rect(alpha, beta)}, used | {alpha, beta}
|
|
|
|
else:
|
|
return merged, used
|
|
|
|
|
|
def bounding_rect(alpha: Rectangle, beta: Rectangle) -> Rectangle:
|
|
"""Returns the smallest rectangle that contains both rectangles."""
|
|
return Rectangle(
|
|
min(alpha.x1, beta.x1),
|
|
min(alpha.y1, beta.y1),
|
|
max(alpha.x2, beta.x2),
|
|
max(alpha.y2, beta.y2),
|
|
)
|