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), )