Commit b84812c4 authored by njean42's avatar njean42
Browse files

Improved panel splitting.

parent 246ee8f1
......@@ -10,6 +10,9 @@ from lib.panel import Panel
class NotAnImageException (Exception):
pass
class Kumiko:
options = {}
......@@ -55,67 +58,37 @@ class Kumiko:
print(len(filenames),'files')
i = -1
for filename in filenames:
for filename in sorted(filenames):
i += 1
if self.options['progress']:
print("\t",urls[i] if urls else filename)
try:
infos.append(self.parse_image(filename,url=urls[i] if urls else None))
except Exception:
except NotAnImageException:
print("Not an image, will be ignored: {}".format(filename), file=sys.stderr)
pass # this file is not an image, will not be part of the results
return infos
def dots_are_close(self,dot1,dot2):
max_dist = self.gutterThreshold * 2
return abs(dot1[0]-dot2[0]) < max_dist and abs(dot1[1]-dot2[1]) < max_dist
def split_polygon(self,polygon):
close_dots = []
for i in range(len(polygon)-1):
all_close = True
for j in range(i+1,len(polygon)):
dot1 = polygon[i][0]
dot2 = polygon[j][0]
subpanel_colours = [(0,255,0),(255,0,0),(200,200,0),(200,0,200),(0,200,200),(150,150,150)]
def split_panels(self,panels,img,contourSize):
new_panels = []
old_panels = []
for p in panels:
new = p.split()
if new != None:
old_panels.append(p)
new_panels += new
if self.dots_are_close(dot1,dot2):
if not all_close:
close_dots.append([i,j])
else:
all_close = False
if len(close_dots) == 0:
return [polygon]
# take the dots that shouldn't be close, i.e. those with the most hops in between (other dots)
best_cut = max(close_dots, key=lambda i: abs(i[1]-i[0]) % (len(polygon)-1))
# NOTE: The first (i=0) and last dots are connected. There's at most len(polygon)-1 hops between two dots
poly1len = len(polygon) - best_cut[1] + best_cut[0]
poly2len = best_cut[1] - best_cut[0]
# A panel should have at least three edges
if min(poly1len,poly2len) <= 2:
return [polygon]
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 <= best_cut[0] or i > best_cut[1]:
poly1[x][0] = polygon[i]
x += 1
else:
poly2[y][0] = polygon[i]
y += 1
if self.options['debug_dir']:
for i in range(len(new)):
cv.drawContours(img, [new[n].polygon], 0, self.subpanel_colours[i % len(self.subpanel_colours)], contourSize)
return [poly1,poly2] # TODO: recurse?
for p in old_panels:
panels.remove(p)
panels += new_panels
def deoverlap_panels(self,panels):
......@@ -202,15 +175,14 @@ class Kumiko:
def parse_image(self,filename,url=None):
img = cv.imread(filename)
if not isinstance(img,np.ndarray) or img.size == 0:
raise Exception('File {} is not an image'.format(filename))
raise NotAnImageException('File {} is not an image'.format(filename))
size = list(img.shape[:2])
size.reverse() # get a [width,height] list
infos = {
'filename': url if url else os.path.basename(filename),
'size': size,
'panels': []
'size': size
}
# get license for this file
......@@ -234,50 +206,51 @@ class Kumiko:
contours, hierarchy = cv.findContours(thresh, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)[-2:]
# Get (square) panels out of contours
contourSize = int(sum(infos['size']) / 2 * 0.004)
panels = []
for contour in contours:
arclength = cv.arcLength(contour,True)
epsilon = 0.01 * arclength
epsilon = 0.001 * arclength
approx = cv.approxPolyDP(contour,epsilon,True)
# See if this panel can be cut into several (two non-consecutive points are close)
polygons = self.split_polygon(approx)
i = -1
for p in polygons:
i += 1
x,y,w,h = cv.boundingRect(p)
# exclude very small panels
if w < infos['size'][0] * self.options['min_panel_size_ratio'] or h < infos['size'][1] * self.options['min_panel_size_ratio']:
continue
if self.options['debug_dir']:
contourSize = int(sum(infos['size']) / 2 * 0.004)
if len(polygons) == 1:
cv.drawContours(img, [p], 0, (0,0,255), contourSize)
else:
cv.drawContours(img, [p], 0, [(0,255,0),(255,0,0)][i], contourSize)
panel = Panel([x,y,w,h], self.gutterThreshold)
infos['panels'].append(panel)
panel = Panel(polygon=approx)
# exclude very small panels
if panel.w < infos['size'][0] * self.options['min_panel_size_ratio'] or panel.h < infos['size'][1] * self.options['min_panel_size_ratio']:
continue
if self.options['debug_dir']:
cv.drawContours(img, [approx], 0, (0,0,255), contourSize)
panels.append(Panel(polygon=approx))
# merge panels that shouldn't have been split (speech bubble diving in a panel)
self.merge_panels(infos['panels'])
# See if panels can be cut into several (two non-consecutive points are close)
self.split_panels(panels,img,contourSize)
# cutting polygons may result in panels slightly overlapping, de-overlap them
self.deoverlap_panels(infos['panels'])
# Merge panels that shouldn't have been split (speech bubble diving in a panel)
self.merge_panels(panels)
infos['panels'].sort() # TODO: remove
self.expand_panels(infos['panels'])
# splitting polygons may result in panels slightly overlapping, de-overlap them
self.deoverlap_panels(panels)
if len(infos['panels']) == 0:
infos['panels'].append( Panel([0,0,infos['size'][0],infos['size'][1]], self.gutterThreshold) );
# get actual gutters before expanding panels
actual_gutters = Kumiko.actual_gutters(panels)
infos['gutters'] = [actual_gutters['x'],actual_gutters['y']]
# Number infos['panels'] comics-wise (left to right for now)
infos['panels'].sort()
panels.sort() # TODO: remove
self.expand_panels(panels)
if len(panels) == 0:
panels.append( Panel([0,0,infos['size'][0],infos['size'][1]]) );
# Number panels comics-wise (left to right for now)
panels.sort()
# Simplify panels back to lists (x,y,w,h)
infos['panels'] = list(map(lambda p: p.to_xywh(), infos['panels']))
panels = list(map(lambda p: p.to_xywh(), panels))
infos['panels'] = panels
# write panel numbers on debug image
if (self.options['debug_dir']):
......
import sys
import cv2 as cv
import numpy as np
class Panel:
def __init__(self, xywh, gutterThreshold):
def __init__(self, xywh=None, polygon=None):
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)
for d in ['x','y','r','b']: super().__setattr__(d,0) # dummy init so that all four edges have a value (see __setattr__)
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.gutterThreshold = gutterThreshold
self.polygon = polygon
def to_xywh(self):
return [self.x, self.y, self.w, self.h]
def __eq__(self, other):
return \
abs(self.x-other.x) < self.gutterThreshold and \
abs(self.y-other.y) < self.gutterThreshold and \
abs(self.r-other.r) < self.gutterThreshold and \
abs(self.b-other.b) < self.gutterThreshold
abs(self.x-other.x) < self.wt and \
abs(self.y-other.y) < self.ht and \
abs(self.r-other.r) < self.wt and \
abs(self.b-other.b) < self.ht
def __lt__(self, other):
# panel is above other
if other.y >= self.b - self.gutterThreshold and other.y >= self.y - self.gutterThreshold:
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.gutterThreshold and self.y >= other.y - self.gutterThreshold:
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.gutterThreshold and other.x >= self.x - self.gutterThreshold:
if other.x >= self.r - self.wt and other.x >= self.x - self.wt:
return True
# panel is right from other
if self.x >= other.r - self.gutterThreshold and self.x >= other.x - self.gutterThreshold:
if self.x >= other.r - self.wt and self.x >= other.x - self.wt:
return False
return True # should not happen, TODO: raise an exception?
......@@ -49,15 +58,16 @@ class Panel:
def __str__(self): return "[left: {}, right: {}, top: {}, bottom: {} ({}x{})]".format(self.x,self.r,self.y,self.b,self.w,self.h)
def __setattr__(self,name,value):
if name in ['w','h']:
print("Fatal error, setting a panel's width or height is not allowed")
sys.exit(1)
if name in ['w','h','wt','ht']:
raise Exception("Fatal error, setting a panel's width or height is not allowed");
super().__setattr__(name, value)
# update width and height
if name in ['x','y','r','b']:
super().__setattr__('w',self.r - self.x)
super().__setattr__('wt',self.w / 10)
super().__setattr__('h',self.b - self.y)
super().__setattr__('ht',self.h / 10)
def overlap_panel(self,other):
......@@ -72,7 +82,7 @@ class Panel:
r = min(self.r,other.r)
b = min(self.b,other.b)
return Panel([x,y,r-x,b-y], self.gutterThreshold)
return Panel([x,y,r-x,b-y])
def contains(self,other):
......@@ -115,3 +125,88 @@ class Panel:
'r': self.find_right_panel,
'b': self.find_bottom_panel,
}[d](panels)
def split(self):
if self.polygon is None:
raise Exception('Fatal error, trying to split a Panel with no polygon (not the result of opencv.findContours)')
close_dots = []
for i in range(len(self.polygon)-1):
all_close = True
for j in range(i+1,len(self.polygon)):
dot1 = self.polygon[i][0]
dot2 = self.polygon[j][0]
# elements that join panels together (e.g. speech bubbles) will be ignored (cut out) if their width and height is < min(panelwidth * ratio, panelheight * ratio)
ratio = 0.25
max_dist = min(self.w * ratio, self.h * ratio)
if abs(dot1[0]-dot2[0]) < max_dist and abs(dot1[1]-dot2[1]) < max_dist:
if not all_close:
close_dots.append([i,j])
else:
all_close = False
if len(close_dots) == 0:
return None
# take the close dots that are closest from one another
cuts = sorted(close_dots, key=lambda d:
abs(self.polygon[d[0]][0][0]-self.polygon[d[1]][0][0]) + # dot1.x - dot2.x
abs(self.polygon[d[0]][0][1]-self.polygon[d[1]][0][1]) # dot1.y - dot2.y
)
for cut in cuts:
poly1len = len(self.polygon) - cut[1] + cut[0]
poly2len = cut[1] - cut[0]
# A panel should have at least three edges
if min(poly1len,poly2len) <= 2:
continue
# Construct two subpolygons by distributing the dots around our cut (our close 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(self.polygon)):
if i <= cut[0] or i > cut[1]:
poly1[x][0] = self.polygon[i]
x += 1
else:
poly2[y][0] = self.polygon[i]
y += 1
panel1 = Panel(polygon=poly1)
panel2 = Panel(polygon=poly2)
# Check that subpanels' width and height are not too small
wh_ok = True
for p in [panel1,panel2]:
if p.h / self.h < 0.1:
wh_ok = False
if p.w / self.w < 0.1:
wh_ok = False
if not wh_ok:
continue
# Check that subpanels' area is not too small
area1 = cv.contourArea(poly1)
area2 = cv.contourArea(poly2)
areaRatio = min(area1,area2) / max(area1,area2)
if areaRatio < 0.1:
continue
subpanels1 = panel1.split()
subpanels2 = panel2.split()
# resurse (find subsubpanels in subpanels)
split_panels = []
split_panels += [panel1] if subpanels1 is None else subpanels1
split_panels += [panel2] if subpanels2 is None else subpanels2
return split_panels
return None
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment