avans06's picture
init commit.
8135b6a
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] # 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