Spaces:
Sleeping
Sleeping
| import math | |
| import cv2 as cv | |
| import numpy as np | |
| from lib.segment import Segment | |
| from lib.debug import Debug | |
| class Panel: | |
| def from_xyrb(page, x, y, r, b): | |
| return Panel(page, xywh = [x, y, r - x, b - y]) | |
| def __init__(self, page, xywh = None, polygon = None, splittable = True): | |
| self.page = page | |
| if xywh is None and polygon is None: | |
| raise Exception('Fatal error: no parameter to define Panel boundaries') | |
| if xywh is None: | |
| xywh = cv.boundingRect(polygon) | |
| self.x = xywh[0] # panel's left edge | |
| self.y = xywh[1] # panel's top edge | |
| self.r = self.x + xywh[2] # panel's right edge | |
| self.b = self.y + xywh[3] # panel's bottom edge | |
| self.polygon = polygon | |
| self.splittable = splittable | |
| self.segments = None | |
| self.coverage = None | |
| def w(self): | |
| return self.r - self.x | |
| def h(self): | |
| return self.b - self.y | |
| def diagonal(self): | |
| return Segment((self.x, self.y), (self.r, self.b)) | |
| def wt(self): | |
| return self.w() / 10 | |
| # wt = width threshold (under which two edge coordinates are considered equal) | |
| def ht(self): | |
| return self.h() / 10 | |
| # ht = height threshold | |
| def to_xywh(self): | |
| return [self.x, self.y, self.w(), self.h()] | |
| def __eq__(self, other): | |
| return all( | |
| [ | |
| abs(self.x - other.x) < self.wt(), | |
| abs(self.y - other.y) < self.ht(), | |
| abs(self.r - other.r) < self.wt(), | |
| abs(self.b - other.b) < self.ht(), | |
| ] | |
| ) | |
| def __lt__(self, other): | |
| # panel is above other | |
| if other.y >= self.b - self.ht() and other.y >= self.y - self.ht(): | |
| return True | |
| # panel is below other | |
| if self.y >= other.b - self.ht() and self.y >= other.y - self.ht(): | |
| return False | |
| # panel is left from other | |
| if other.x >= self.r - self.wt() and other.x >= self.x - self.wt(): | |
| return True if self.page.numbering == 'ltr' else False | |
| # panel is right from other | |
| if self.x >= other.r - self.wt() and self.x >= other.x - self.wt(): | |
| return False if self.page.numbering == 'ltr' else True | |
| return True # should not happen, TODO: raise an exception? | |
| def __le__(self, other): | |
| return self.__lt__(other) | |
| def __gt__(self, other): | |
| return not self.__lt__(other) | |
| def __ge__(self, other): | |
| return self.__gt__(other) | |
| def area(self): | |
| return self.w() * self.h() | |
| def __str__(self): | |
| return f"{self.x}x{self.y}-{self.r}x{self.b}" | |
| def __hash__(self): | |
| return hash(self.__str__()) | |
| def is_small(self, extra_ratio = 1): | |
| return any( | |
| [ | |
| self.w() < self.page.img_size[0] * self.page.small_panel_ratio * extra_ratio, | |
| self.h() < self.page.img_size[1] * self.page.small_panel_ratio * extra_ratio, | |
| ] | |
| ) | |
| def is_very_small(self): | |
| return self.is_small(1 / 10) | |
| def overlap_panel(self, other): | |
| if self.x > other.r or other.x > self.r: # panels are left and right from one another | |
| return None | |
| if self.y > other.b or other.y > self.b: # panels are above and below one another | |
| return None | |
| # if we're here, panels overlap at least a bit | |
| x = max(self.x, other.x) | |
| y = max(self.y, other.y) | |
| r = min(self.r, other.r) | |
| b = min(self.b, other.b) | |
| return Panel(self.page, [x, y, r - x, b - y]) | |
| def overlap_area(self, other): | |
| opanel = self.overlap_panel(other) | |
| if opanel is None: | |
| return 0 | |
| return opanel.area() | |
| def overlaps(self, other): | |
| opanel = self.overlap_panel(other) | |
| if opanel is None: | |
| return False | |
| area_ratio = 0.1 | |
| smallest_panel_area = min(self.area(), other.area()) | |
| if smallest_panel_area == 0: # probably a horizontal or vertical segment | |
| return True | |
| return opanel.area() / smallest_panel_area > area_ratio | |
| def contains(self, other): | |
| o_panel = self.overlap_panel(other) | |
| if not o_panel: | |
| return False | |
| # self contains other if their overlapping area is more than 50% of other's area | |
| return o_panel.area() / other.area() > 0.50 | |
| def same_row(self, other): | |
| above, below = sorted([self, other], key = lambda p: p.y) | |
| if below.y > above.b: # stricly above | |
| return False | |
| if below.b < above.b: # contained | |
| return True | |
| # intersect | |
| intersection_y = min(above.b, below.b) - below.y | |
| min_h = min(above.h(), below.h()) | |
| return min_h == 0 or intersection_y / min_h >= 1 / 3 | |
| def same_col(self, other): | |
| left, right = sorted([self, other], key = lambda p: p.x) | |
| if right.x > left.r: # stricly left | |
| return False | |
| if right.r < left.r: # contained | |
| return True | |
| # intersect | |
| intersection_x = min(left.r, right.r) - right.x | |
| min_w = min(left.w(), right.w()) | |
| return min_w == 0 or intersection_x / min_w >= 1 / 3 | |
| def find_top_panel(self): | |
| all_top = list(filter(lambda p: p.b <= self.y and p.same_col(self), self.page.panels)) | |
| return max(all_top, key = lambda p: p.b) if all_top else None | |
| def find_bottom_panel(self): | |
| all_bottom = list(filter(lambda p: p.y >= self.b and p.same_col(self), self.page.panels)) | |
| return min(all_bottom, key = lambda p: p.y) if all_bottom else None | |
| def find_all_left_panels(self): | |
| return list(filter(lambda p: p.r <= self.x and p.same_row(self), self.page.panels)) | |
| def find_left_panel(self): | |
| all_left = self.find_all_left_panels() | |
| return max(all_left, key = lambda p: p.r) if all_left else None | |
| def find_all_right_panels(self): | |
| return list(filter(lambda p: p.x >= self.r and p.same_row(self), self.page.panels)) | |
| def find_right_panel(self): | |
| all_right = self.find_all_right_panels() | |
| return min(all_right, key = lambda p: p.x) if all_right else None | |
| def find_neighbour_panel(self, d): | |
| return { | |
| 'x': self.find_left_panel, | |
| 'y': self.find_top_panel, | |
| 'r': self.find_right_panel, | |
| 'b': self.find_bottom_panel, | |
| }[d]() | |
| def group_with(self, other): | |
| min_x = min(self.x, other.x) | |
| min_y = min(self.y, other.y) | |
| max_r = max(self.r, other.r) | |
| max_b = max(self.b, other.b) | |
| return Panel(self.page, [min_x, min_y, max_r - min_x, max_b - min_y]) | |
| def merge(self, other): | |
| possible_panels = [self] | |
| # expand self in all four directions where other is | |
| if other.x < self.x: | |
| possible_panels.append(Panel.from_xyrb(self.page, other.x, self.y, self.r, self.b)) | |
| if other.r > self.r: | |
| for pp in possible_panels.copy(): | |
| possible_panels.append(Panel.from_xyrb(self.page, pp.x, pp.y, other.r, pp.b)) | |
| if other.y < self.y: | |
| for pp in possible_panels.copy(): | |
| possible_panels.append(Panel.from_xyrb(self.page, pp.x, other.y, pp.r, pp.b)) | |
| if other.b > self.b: | |
| for pp in possible_panels.copy(): | |
| possible_panels.append(Panel.from_xyrb(self.page, pp.x, pp.y, pp.r, other.b)) | |
| # don't take a merged panel that bumps into other panels on page | |
| other_panels = [p for p in self.page.panels if p not in [self, other]] | |
| possible_panels = list(filter(lambda p: not p.bumps_into(other_panels), possible_panels)) | |
| # take the largest merged panel | |
| return max(possible_panels, key = lambda p: p.area()) if len(possible_panels) > 0 else self | |
| def is_close(self, other): | |
| c1x = self.x + self.w() / 2 | |
| c1y = self.y + self.h() / 2 | |
| c2x = other.x + other.w() / 2 | |
| c2y = other.y + other.h() / 2 | |
| return all( | |
| [ | |
| abs(c1x - c2x) <= (self.w() + other.w()) * 0.75, | |
| abs(c1y - c2y) <= (self.h() + other.h()) * 0.75, | |
| ] | |
| ) | |
| def bumps_into(self, other_panels): | |
| for other in other_panels: | |
| if other == self: | |
| continue | |
| if self.overlaps(other): | |
| return True | |
| return False | |
| def contains_segment(self, segment): | |
| other = Panel.from_xyrb(None, *segment.to_xyrb()) | |
| return self.overlaps(other) | |
| def get_segments(self): | |
| if self.segments is not None: | |
| return self.segments | |
| self.segments = list(filter(lambda s: self.contains_segment(s), self.page.segments)) | |
| return self.segments | |
| def split(self): | |
| if self.splittable is False: | |
| return None | |
| split = self._cached_split() | |
| if split is None: | |
| self.splittable = False | |
| return split | |
| def _cached_split(self): | |
| if self.polygon is None: | |
| return None | |
| if self.is_small(extra_ratio = 2): # panel should be splittable in two non-small subpanels | |
| return None | |
| min_hops = 3 | |
| max_dist_x = int(self.w() / 3) | |
| max_dist_y = int(self.h() / 3) | |
| max_diagonal = math.sqrt(max_dist_x**2 + max_dist_y**2) | |
| dots_along_lines_dist = max_diagonal / 5 | |
| min_dist_between_dots_x = max_dist_x / 10 | |
| min_dist_between_dots_y = max_dist_y / 10 | |
| # Compose modified polygon to optimise splits | |
| original_polygon = np.copy(self.polygon) | |
| polygon = np.ndarray(shape = (0, 1, 2), dtype = int, order = 'F') | |
| intermediary_dots = [] | |
| extra_dots = [] | |
| for i in range(len(original_polygon)): | |
| j = (i + 1) % len(original_polygon) | |
| dot1 = tuple(original_polygon[i][0]) | |
| dot2 = tuple(original_polygon[j][0]) | |
| seg = Segment(dot1, dot2) | |
| # merge nearby dots together | |
| if seg.dist_x() < min_dist_between_dots_x and seg.dist_y() < min_dist_between_dots_y: | |
| original_polygon[j][0] = seg.center() | |
| continue | |
| polygon = np.append(polygon, [[dot1]], axis = 0) | |
| # Add dots on *long* edges, by projecting other polygon dots on this segment | |
| add_dots = [] | |
| # should be splittable in [dot1, dot1b(?), projected_dot3, dot2b(?), dot2] | |
| if seg.dist() < dots_along_lines_dist * 2: | |
| continue | |
| for k, dot3 in enumerate(original_polygon): | |
| if abs(k - i) < min_hops: | |
| continue | |
| projected_dot3 = seg.projected_point(dot3) | |
| # Segment should be able to contain projected_dot3 | |
| if not seg.may_contain(projected_dot3): | |
| continue | |
| # dot3 should be close to current segment − distance(dot3, projected_dot3) should be short | |
| project = Segment(dot3[0], projected_dot3) | |
| if project.dist_x() > max_dist_x or project.dist_y() > max_dist_y: | |
| continue | |
| # append dot3 as intermediary dot on segment(dot1, dot2) | |
| add_dots.append(projected_dot3) | |
| intermediary_dots.append(projected_dot3) | |
| # Add also a dot near each end of the segment (provoke segment matching) | |
| alpha_x = math.acos(seg.dist_x(keep_sign = True) / seg.dist()) | |
| alpha_y = math.asin(seg.dist_y(keep_sign = True) / seg.dist()) | |
| dist_x = int(math.cos(alpha_x) * dots_along_lines_dist) | |
| dist_y = int(math.sin(alpha_y) * dots_along_lines_dist) | |
| dot1b = (dot1[0] + dist_x, dot1[1] + dist_y) | |
| # if len(intermediary_dots) == 0 or Segment(dot1b, intermediary_dots[0]).dist() > dots_along_lines_dist: | |
| add_dots.append(dot1b) | |
| extra_dots.append(dot1b) | |
| dot2b = (dot2[0] - dist_x, dot2[1] - dist_y) | |
| # if len(intermediary_dots) == 0 or Segment(dot2b, intermediary_dots[-1]).dist() > dots_along_lines_dist: | |
| add_dots.append(dot2b) | |
| extra_dots.append(dot2b) | |
| for dot in sorted(add_dots, key = lambda dot: Segment(dot1, dot).dist()): | |
| polygon = np.append(polygon, [[dot]], axis = 0) | |
| # Re-merge nearby dots together | |
| original_polygon = np.copy(polygon) | |
| polygon = np.ndarray(shape = (0, 1, 2), dtype = int, order = 'F') | |
| for i in range(len(original_polygon)): | |
| j = (i + 1) % len(original_polygon) | |
| dot1 = tuple(original_polygon[i][0]) | |
| dot2 = tuple(original_polygon[j][0]) | |
| seg = Segment(dot1, dot2) | |
| # merge nearby dots together | |
| if seg.dist_x() < min_dist_between_dots_x and seg.dist_y() < min_dist_between_dots_y: | |
| intermediary_dots = [dot for dot in intermediary_dots if dot not in [dot1, dot2]] | |
| extra_dots = [dot for dot in extra_dots if dot not in [dot1, dot2]] | |
| original_polygon[j][0] = seg.center() | |
| continue | |
| polygon = np.append(polygon, [[dot1]], axis = 0) | |
| Debug.draw_polygon(polygon) | |
| Debug.draw_dots(intermediary_dots, Debug.colours['red']) | |
| Debug.draw_dots(extra_dots, Debug.colours['yellow']) | |
| Debug.add_image(f"Composed polygon {self} ({len(polygon)} dots, {len(intermediary_dots)} intermediary)") | |
| # Find dots nearby one another | |
| nearby_dots = [] | |
| for i in range(len(polygon) - min_hops): | |
| for j in range(i + min_hops, len(polygon)): | |
| dot1 = polygon[i][0] | |
| dot2 = polygon[j][0] | |
| seg = Segment(dot1, dot2) | |
| if seg.dist_x() <= max_dist_x and seg.dist_y() <= max_dist_y: | |
| nearby_dots.append([i, j]) | |
| if len(nearby_dots) == 0: | |
| return None | |
| Debug.draw_nearby_dots(polygon, nearby_dots) | |
| Debug.add_image(f"Nearby dots ({len(nearby_dots)})") | |
| splits = [] | |
| for dots in nearby_dots: | |
| poly1len = len(polygon) - dots[1] + dots[0] | |
| poly2len = dots[1] - dots[0] | |
| # A panel should have at least three edges | |
| if min(poly1len, poly2len) <= 2: | |
| continue | |
| # Construct two subpolygons by distributing the dots around our nearby dots | |
| poly1 = np.zeros(shape = (poly1len, 1, 2), dtype = int) | |
| poly2 = np.zeros(shape = (poly2len, 1, 2), dtype = int) | |
| x = y = 0 | |
| for i in range(len(polygon)): | |
| if i <= dots[0] or i > dots[1]: | |
| poly1[x][0] = polygon[i] | |
| x += 1 | |
| else: | |
| poly2[y][0] = polygon[i] | |
| y += 1 | |
| panel1 = Panel(self.page, polygon = poly1) | |
| panel2 = Panel(self.page, polygon = poly2) | |
| if panel1.is_small() or panel2.is_small(): | |
| continue | |
| if panel1 == self or panel2 == self: | |
| continue | |
| if panel1.overlaps(panel2): | |
| continue | |
| split_segment = Segment.along_polygon(polygon, dots[0], dots[1]) | |
| split = Split(self, panel1, panel2, split_segment) | |
| if split not in splits: | |
| splits.append(split) | |
| Debug.draw_segments([split.segment for split in splits], Debug.colours['red'], size = 2) | |
| Debug.add_image(f"Splits ({len(splits)})") | |
| splits = list(filter(lambda split: split.segments_coverage() > 50 / 100, splits)) | |
| if len(splits) == 0: | |
| return None | |
| # return the split that best matches segments (~panel edges) | |
| best_split = max(splits, key = lambda split: split.covered_dist) | |
| return best_split | |
| class Split: | |
| def __init__(self, panel, subpanel1, subpanel2, split_segment): | |
| self.panel = panel | |
| self.subpanels = [subpanel1, subpanel2] | |
| self.segment = split_segment | |
| self.matching_segments = self.segment.intersect_all(self.panel.get_segments()) | |
| self.covered_dist = sum(map(lambda s: s.dist(), self.matching_segments)) | |
| def __eq__(self, other): | |
| return self.segment == other.segment | |
| def segments_coverage(self): | |
| segment_dist = self.segment.dist() | |
| return self.covered_dist / segment_dist if segment_dist else 0 | |