|
import math |
|
import cv2 as cv |
|
import numpy as np |
|
|
|
from lib.segment import Segment |
|
from lib.debug import Debug |
|
|
|
|
|
class Panel: |
|
|
|
@staticmethod |
|
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] |
|
self.y = xywh[1] |
|
self.r = self.x + xywh[2] |
|
self.b = self.y + xywh[3] |
|
|
|
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 |
|
|
|
|
|
def ht(self): |
|
return self.h() / 10 |
|
|
|
|
|
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): |
|
|
|
if other.y >= self.b - self.ht() and other.y >= self.y - self.ht(): |
|
return True |
|
|
|
|
|
if self.y >= other.b - self.ht() and self.y >= other.y - self.ht(): |
|
return False |
|
|
|
|
|
if other.x >= self.r - self.wt() and other.x >= self.x - self.wt(): |
|
return True if self.page.numbering == 'ltr' else False |
|
|
|
|
|
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 |
|
|
|
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: |
|
return None |
|
if self.y > other.b or other.y > self.b: |
|
return None |
|
|
|
|
|
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: |
|
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 |
|
|
|
|
|
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: |
|
return False |
|
|
|
if below.b < above.b: |
|
return True |
|
|
|
|
|
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: |
|
return False |
|
|
|
if right.r < left.r: |
|
return True |
|
|
|
|
|
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] |
|
|
|
|
|
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)) |
|
|
|
|
|
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)) |
|
|
|
|
|
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): |
|
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 |
|
|
|
|
|
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) |
|
|
|
|
|
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 = [] |
|
|
|
|
|
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) |
|
|
|
|
|
if not seg.may_contain(projected_dot3): |
|
continue |
|
|
|
|
|
project = Segment(dot3[0], projected_dot3) |
|
if project.dist_x() > max_dist_x or project.dist_y() > max_dist_y: |
|
continue |
|
|
|
|
|
add_dots.append(projected_dot3) |
|
intermediary_dots.append(projected_dot3) |
|
|
|
|
|
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) |
|
|
|
add_dots.append(dot1b) |
|
extra_dots.append(dot1b) |
|
|
|
dot2b = (dot2[0] - dist_x, dot2[1] - dist_y) |
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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)") |
|
|
|
|
|
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] |
|
|
|
|
|
if min(poly1len, poly2len) <= 2: |
|
continue |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|