From 3d83489819d1fa3bed1609a5100edd1458f189f1 Mon Sep 17 00:00:00 2001 From: Matthias Bisping Date: Tue, 10 Jan 2023 11:14:15 +0100 Subject: [PATCH] Refactoring: Make single pass rectangle merging stateless --- cv_analysis/layout_parsing.py | 4 +-- cv_analysis/utils/merging.py | 47 ++++++++++++++++++++++++++-------- cv_analysis/utils/rectangle.py | 3 +++ cv_analysis/utils/spacial.py | 2 +- 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/cv_analysis/layout_parsing.py b/cv_analysis/layout_parsing.py index 85da4d4..2c92d84 100644 --- a/cv_analysis/layout_parsing.py +++ b/cv_analysis/layout_parsing.py @@ -14,7 +14,7 @@ from cv_analysis.utils.common import ( fill_rectangles, ) from cv_analysis.utils.conversion import contour_to_rectangle -from cv_analysis.utils.merging import connect_related_rectangles +from cv_analysis.utils.merging import merge_related_rectangles from cv_analysis.utils.postprocessing import remove_included, has_no_parent from cv_analysis.utils.rectangle import Rectangle @@ -23,7 +23,7 @@ def parse_layout(image: np.array) -> List[Rectangle]: rectangles = rcompose( find_segments, remove_included, - connect_related_rectangles, + merge_related_rectangles, remove_included, )(image) diff --git a/cv_analysis/utils/merging.py b/cv_analysis/utils/merging.py index 15f84a6..d9c5067 100644 --- a/cv_analysis/utils/merging.py +++ b/cv_analysis/utils/merging.py @@ -1,25 +1,52 @@ +from functools import reduce from itertools import combinations -from typing import List +from typing import List, Tuple, Set from cv_analysis.utils import until, make_merger_sentinel from cv_analysis.utils.rectangle import Rectangle -from cv_analysis.utils.spacial import is_related +from cv_analysis.utils.spacial import related -def connect_related_rectangles(rectangles: List[Rectangle]) -> List[Rectangle]: +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]: - for alpha, beta in combinations(rectangles, 2): - if is_related(alpha, beta): - rectangles.remove(alpha) - rectangles.remove(beta) - rectangles.append(bounding_rect(alpha, beta)) - return rectangles - return rectangles + """Merges rectangles that are related to each other, but does not iterate on the results.""" + + def merge_if_related( + acc: Tuple[Set[Rectangle], Set[Rectangle]], + pair: Tuple[Rectangle, Rectangle], + ) -> Tuple[Set[Rectangle], Set[Rectangle]]: + """Merges two rectangles if they are related, otherwise returns the accumulator unchanged.""" + alpha, beta = pair + merged, used = acc + + def unused(*args) -> bool: + return not used & {*args} + + if unused(alpha) and unused(beta) and related(alpha, beta): + return merged | {bounding_rect(alpha, beta)}, used | {alpha, beta} + + else: + return merged, used + + rectangles = set(rectangles) + merged, used = reduce(merge_if_related, combinations(rectangles, 2), (set(), set())) + + return list(merged | rectangles - used) + + # for alpha, beta in combinations(rectangles, 2): + # if related(alpha, beta): + # rectangles.remove(alpha) + # rectangles.remove(beta) + # rectangles.append(bounding_rect(alpha, beta)) + # return rectangles + # return rectangles def bounding_rect(alpha: Rectangle, beta: Rectangle) -> Rectangle: diff --git a/cv_analysis/utils/rectangle.py b/cv_analysis/utils/rectangle.py index 8fa7f2c..b2d8834 100644 --- a/cv_analysis/utils/rectangle.py +++ b/cv_analysis/utils/rectangle.py @@ -20,6 +20,9 @@ class Rectangle: self.__x2 = nearest_valid(x2) self.__y2 = nearest_valid(y2) + def __repr__(self): + return f"Rectangle({self.x1}, {self.y1}, {self.x2}, {self.y2})" + @property def x1(self): return self.__x1 diff --git a/cv_analysis/utils/spacial.py b/cv_analysis/utils/spacial.py index 094db8a..b3a0835 100644 --- a/cv_analysis/utils/spacial.py +++ b/cv_analysis/utils/spacial.py @@ -273,7 +273,7 @@ def intersection_along_axis(alpha, beta, axis): return intersection -def is_related(alpha: Rectangle, beta: Rectangle): +def related(alpha: Rectangle, beta: Rectangle): return close(alpha, beta) or overlap(alpha, beta)