|
from pathlib import Path
|
|
import sys
|
|
|
|
import cv2 as cv
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
KERNEL_SIZE = 7
|
|
BORDER_SIZE = 10
|
|
|
|
|
|
def panel_process_image(img: Image.Image):
|
|
"""Preprocesses an image to make it easier to find panels.
|
|
|
|
Args:
|
|
img: The image to preprocess.
|
|
|
|
Returns:
|
|
The preprocessed image.
|
|
"""
|
|
|
|
img_gray = cv.cvtColor(np.array(img), cv.COLOR_BGR2GRAY)
|
|
img_gray = cv.GaussianBlur(img_gray, (KERNEL_SIZE, KERNEL_SIZE), 0)
|
|
img_gray = cv.threshold(img_gray, 200, 255, cv.THRESH_BINARY)[1]
|
|
|
|
|
|
img_gray = cv.copyMakeBorder(
|
|
img_gray,
|
|
BORDER_SIZE,
|
|
BORDER_SIZE,
|
|
BORDER_SIZE,
|
|
BORDER_SIZE,
|
|
cv.BORDER_CONSTANT,
|
|
value=255,
|
|
)
|
|
|
|
img_gray = cv.bitwise_not(img_gray)
|
|
return img_gray
|
|
|
|
|
|
def remove_contained_contours(polygons):
|
|
"""Removes polygons from a list if any completely contain the other.
|
|
|
|
Args:
|
|
polygons: A list of polygons.
|
|
|
|
Returns:
|
|
A list of polygons with any contained polygons removed.
|
|
"""
|
|
|
|
|
|
filtered_polygons = []
|
|
|
|
|
|
for polygon in polygons:
|
|
|
|
contains = False
|
|
for other_polygon in polygons:
|
|
|
|
if np.array_equal(other_polygon, polygon):
|
|
continue
|
|
rect1 = cv.boundingRect(other_polygon)
|
|
rect2 = cv.boundingRect(polygon)
|
|
|
|
if (
|
|
rect2[0] >= rect1[0]
|
|
and rect2[1] >= rect1[1]
|
|
and rect2[0] + rect2[2] <= rect1[0] + rect1[2]
|
|
and rect2[1] + rect2[3] <= rect1[1] + rect1[3]
|
|
):
|
|
contains = True
|
|
break
|
|
|
|
|
|
|
|
if not contains:
|
|
filtered_polygons.append(polygon)
|
|
|
|
return filtered_polygons
|
|
|
|
|
|
def calc_panel_contours(im: Image.Image):
|
|
img_gray = panel_process_image(im)
|
|
contours_raw, hierarchy = cv.findContours(
|
|
img_gray, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE
|
|
)
|
|
contours = contours_raw
|
|
min_area = 10000
|
|
contours = [i for i in contours if cv.contourArea(i) > min_area]
|
|
contours = [cv.convexHull(i) for i in contours]
|
|
contours = remove_contained_contours(contours)
|
|
|
|
|
|
contours = [i + np.array([[-BORDER_SIZE, -BORDER_SIZE]]) for i in contours]
|
|
|
|
|
|
contours = order_panels(contours, img_gray)
|
|
return contours
|
|
|
|
|
|
def determine_panel_order_from_contours(contours):
|
|
"""
|
|
build a tree of regions that are determined vertically
|
|
order by an n like pattern
|
|
"""
|
|
|
|
|
|
def draw_contours(im, contours):
|
|
"""Debugging, draws the contours on the image."""
|
|
colors = [
|
|
(255, 0, 0),
|
|
(0, 255, 0),
|
|
(0, 0, 255),
|
|
]
|
|
|
|
im_contour = np.array(im)
|
|
|
|
for i, contour in enumerate(range(len(contours))):
|
|
color = colors[i % len(colors)]
|
|
im_contour = cv.drawContours(im_contour, contours, i, color, 4, cv.LINE_AA)
|
|
|
|
x, y, _, _ = cv.boundingRect(contours[i])
|
|
cv.putText(
|
|
im_contour,
|
|
str(i),
|
|
(x + 50, y + 50),
|
|
cv.FONT_HERSHEY_SIMPLEX,
|
|
1,
|
|
color,
|
|
2,
|
|
cv.LINE_AA,
|
|
)
|
|
|
|
img = Image.fromarray(im_contour)
|
|
|
|
return img
|
|
|
|
|
|
def save_draw_contours(pth: Path | str):
|
|
if str:
|
|
pth = Path(pth)
|
|
pth_out = pth.parent / (pth.stem + "-contours")
|
|
|
|
if not pth_out.exists():
|
|
pth_out.mkdir()
|
|
|
|
|
|
|
|
pths = [i for i in pth.iterdir() if i.suffix in [".png", ".jpg", ".jpeg"]]
|
|
for t in pths:
|
|
print(t)
|
|
im = Image.open(t)
|
|
contours = calc_panel_contours(im)
|
|
|
|
img_panels = draw_contours(im, contours)
|
|
f_name = t.stem + t.suffix
|
|
img_panels.save(pth_out / f_name)
|
|
|
|
|
|
def order_panels(contours, img_gray):
|
|
"""Orders the panels in a comic book page.
|
|
|
|
Args:
|
|
contours: A list of contours, where each contour is a list of points.
|
|
|
|
Returns:
|
|
A list of contours, where each contour is a list of points, ordered by
|
|
their vertical position.
|
|
"""
|
|
|
|
|
|
bounding_boxes = [cv.boundingRect(contour) for contour in contours]
|
|
|
|
|
|
groups_indices = generate_vertical_bounding_box_groups_indices(bounding_boxes)
|
|
|
|
c = []
|
|
|
|
for group in groups_indices:
|
|
|
|
|
|
cs = [bounding_boxes[i] for i in group]
|
|
ymax, xmax = img_gray.shape
|
|
order_scores = [1 * (ymax - i[1]) + i[0] * 1 for i in cs]
|
|
|
|
|
|
combined_list = list(zip(group, order_scores))
|
|
sorted_list = sorted(combined_list, key=lambda x: x[1], reverse=True)
|
|
c.extend(sorted_list)
|
|
|
|
ordered_contours = [contours[i[0]] for i in c]
|
|
return ordered_contours
|
|
|
|
|
|
def generate_vertical_bounding_box_groups_indices(bounding_boxes):
|
|
"""Generates groups of vertically overlapping bounding boxes.
|
|
|
|
Args:
|
|
bounding_boxes: A list of bounding boxes, where each bounding box is a tuple
|
|
of (x, y, width, height).
|
|
|
|
Returns:
|
|
A list of groups, where each group is a list of bounding boxes that overlap
|
|
vertically.
|
|
"""
|
|
|
|
|
|
|
|
bbox_inds = np.argsort([i[1] for i in bounding_boxes])
|
|
|
|
|
|
groups = [[bbox_inds[0]]]
|
|
for i in bbox_inds[1:]:
|
|
is_old_group = False
|
|
bbox = bounding_boxes[i]
|
|
start1 = bbox[1]
|
|
end1 = bbox[1] + bbox[3]
|
|
for n, group in enumerate(groups):
|
|
for ind in group:
|
|
_bbox = bounding_boxes[ind]
|
|
start2 = _bbox[1]
|
|
end2 = _bbox[1] + _bbox[3]
|
|
|
|
|
|
if check_overlap((start1, end1), (start2, end2)):
|
|
groups[n] = group + [i]
|
|
is_old_group = True
|
|
break
|
|
|
|
if is_old_group:
|
|
break
|
|
else:
|
|
groups.append([i])
|
|
return groups
|
|
|
|
|
|
def check_overlap(range1, range2):
|
|
|
|
if range1[1] < range2[0]:
|
|
return False
|
|
|
|
elif range1[0] > range2[1]:
|
|
return False
|
|
|
|
else:
|
|
return True
|
|
|
|
|
|
if __name__ == "__main__":
|
|
save_draw_contours(sys.argv[1])
|
|
|