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