from itertools import combinations, starmap, product from typing import Iterable def is_near_enough(rect_pair, max_gap=14): x1, y1, w1, h1 = rect_pair[0] x2, y2, w2, h2 = rect_pair[1] return any( [ abs(x1 - (x2 + w2)) <= max_gap, abs(x2 - (x1 + w1)) <= max_gap, abs(y2 - (y1 + h1)) <= max_gap, abs(y1 - (y2 + h2)) <= max_gap, ] ) def is_overlapping(rect_pair): x1, y1, w1, h1 = rect_pair[0] x2, y2, w2, h2 = rect_pair[1] dx = min(x1 + w1, x2 + w2) - max(x1, x2) dy = min(y1 + h1, y2 + h2) - max(y1, y2) return True if (dx >= 0) and (dy >= 0) else False def is_on_same_line(rect_pair): x1, y1, w1, h1 = rect_pair[0] x2, y2, w2, h2 = rect_pair[1] return any( [ any([abs(y1 - y2) <= 10, abs(y1 + h1 - (y2 + h2)) <= 10]), any([y2 <= y1 and y1 + h1 <= y2 + h2, y1 <= y2 and y2 + h2 <= y1 + h1]), ] ) def has_correct_position1(rect_pair): x1, y1, w1, h1 = rect_pair[0] x2, y2, w2, h2 = rect_pair[1] return any( [ any( [abs(x1 - x2) <= 10, abs(y1 - y2) <= 10, abs(x1 + w1 - (x2 + w2)) <= 10, abs(y1 + h1 - (y2 + h2)) <= 10] ), any( [ y2 <= y1 and y1 + h1 <= y2 + h2, y1 <= y2 and y2 + h2 <= y1 + h1, x2 <= x1 and x1 + w1 <= x2 + w2, x1 <= x2 and x2 + w2 <= x1 + w1, ] ), ] ) def is_related(rect_pair): return (is_near_enough(rect_pair) and has_correct_position1(rect_pair)) or is_overlapping(rect_pair) def fuse_rects(rect1, rect2): if rect1 == rect2: return rect1 x1, y1, w1, h1 = rect1 x2, y2, w2, h2 = rect2 topleft = list(min(product([x1, x2], [y1, y2]))) bottomright = list(max(product([x1 + w1, x2 + w2], [y1 + h1, y2 + h2]))) w = [bottomright[0] - topleft[0]] h = [bottomright[1] - topleft[1]] return tuple(topleft + w + h) def rects_not_the_same(r): return r[0] != r[1] def find_related_rects(rects): rect_pairs = list(filter(is_related, combinations(rects, 2))) rect_pairs = list(filter(rects_not_the_same, rect_pairs)) if not rect_pairs: return [], rects rel_rects = list(set([rect for pair in rect_pairs for rect in pair])) unrel_rects = [rect for rect in rects if rect not in rel_rects] return rect_pairs, unrel_rects def connect_related_rects(rects): rects_to_connect, rects_new = find_related_rects(rects) while len(rects_to_connect) > 0: rects_fused = list(starmap(fuse_rects, rects_to_connect)) rects_fused = list(dict.fromkeys(rects_fused)) if len(rects_fused) == 1: rects_new += rects_fused rects_fused = [] rects_to_connect, connected_rects = find_related_rects(rects_fused) rects_new += connected_rects if len(rects_to_connect) > 1 and len(set(rects_to_connect)) == 1: rects_new.append(rects_fused[0]) rects_to_connect = [] return rects_new def connect_related_rects2(rects: Iterable[tuple]): rects = list(rects) current_idx = 0 while True: if current_idx + 1 >= len(rects) or len(rects) <= 1: break merge_happened = False current_rect = rects.pop(current_idx) for idx, maybe_related_rect in enumerate(rects): if is_related((current_rect, maybe_related_rect)): current_rect = fuse_rects(current_rect, maybe_related_rect) rects.pop(idx) merge_happened = True break rects.insert(0, current_rect) if not merge_happened: current_idx += 1 elif merge_happened: current_idx = 0 return rects