import math from dataclasses import dataclass SQRT2 = math.sqrt(2) @dataclass class Circle: x: int y: int r: int @dataclass class Corners: top_left: Circle top_right: Circle bottom_left: Circle bottom_right: Circle @classmethod def from_opposite_corners(cls, top_left: Circle, bottom_right: Circle) -> "Corners": r_tl = top_left.r r_br = bottom_right.r r_max = max(r_tl, r_br) x_tl, y_tl = float(top_left.x), float(top_left.y) x_br, y_br = float(bottom_right.x), float(bottom_right.y) if r_tl < r_max: diff = r_max - r_tl dx = x_br - x_tl dy = y_br - y_tl dist = math.sqrt(dx * dx + dy * dy) if dist > 0: x_tl = x_tl + diff * dx / dist y_tl = y_tl + diff * dy / dist if r_br < r_max: diff = r_max - r_br dx = x_tl - x_br dy = y_tl - y_br dist = math.sqrt(dx * dx + dy * dy) if dist > 0: x_br = x_br + diff * dx / dist y_br = y_br + diff * dy / dist x_tl, y_tl = int(round(x_tl)), int(round(y_tl)) x_br, y_br = int(round(x_br)), int(round(y_br)) return cls( top_left=Circle(x_tl, y_tl, r_max), top_right=Circle(x_br, y_tl, r_max), bottom_left=Circle(x_tl, y_br, r_max), bottom_right=Circle(x_br, y_br, r_max), ) @classmethod def from_boxes(cls, boxes: list[tuple[int, int, int, int]]) -> "Corners | None": if len(boxes) < 4: return None box_centers = [] for x1, y1, x2, y2 in boxes[:4]: cx = (x1 + x2) / 2 cy = (y1 + y2) / 2 box_centers.append((cx, cy, x1, y1, x2, y2)) sorted_by_y = sorted(box_centers, key=lambda b: b[1]) top_two = sorted(sorted_by_y[:2], key=lambda b: b[0]) bottom_two = sorted(sorted_by_y[2:], key=lambda b: b[0]) def box_to_circle(box_data: tuple, corner: str) -> Circle: _, _, x1, y1, x2, y2 = box_data r = (x2 - x1 + y2 - y1) // 2 if corner == "top_left": return Circle(x2, y2, r) elif corner == "top_right": return Circle(x1, y2, r) elif corner == "bottom_left": return Circle(x2, y1, r) else: return Circle(x1, y1, r) return cls( top_left=box_to_circle(top_two[0], "top_left"), top_right=box_to_circle(top_two[1], "top_right"), bottom_left=box_to_circle(bottom_two[0], "bottom_left"), bottom_right=box_to_circle(bottom_two[1], "bottom_right"), ) def to_dict(self) -> dict: return { "top_left": {"x": self.top_left.x, "y": self.top_left.y, "r": self.top_left.r}, "top_right": {"x": self.top_right.x, "y": self.top_right.y, "r": self.top_right.r}, "bottom_left": {"x": self.bottom_left.x, "y": self.bottom_left.y, "r": self.bottom_left.r}, "bottom_right": {"x": self.bottom_right.x, "y": self.bottom_right.y, "r": self.bottom_right.r}, } def get_crop_bounds(self) -> tuple[int, int, int, int]: x_left_tl = self.top_left.x - int(self.top_left.r / SQRT2) x_left_bl = self.bottom_left.x - int(self.bottom_left.r / SQRT2) x_left = max(x_left_tl, x_left_bl) x_right_tr = self.top_right.x + int(self.top_right.r / SQRT2) x_right_br = self.bottom_right.x + int(self.bottom_right.r / SQRT2) x_right = min(x_right_tr, x_right_br) y_top_tl = self.top_left.y - int(self.top_left.r / SQRT2) y_top_tr = self.top_right.y - int(self.top_right.r / SQRT2) y_top = max(y_top_tl, y_top_tr) y_bottom_bl = self.bottom_left.y + int(self.bottom_left.r / SQRT2) y_bottom_br = self.bottom_right.y + int(self.bottom_right.r / SQRT2) y_bottom = min(y_bottom_bl, y_bottom_br) return x_left, y_top, x_right, y_bottom