|
|
|
import argparse |
|
from argparse import RawTextHelpFormatter |
|
import os |
|
from os.path import splitext, basename, exists, join |
|
from os import makedirs |
|
|
|
from tqdm import tqdm |
|
import numpy as np |
|
from skimage import measure |
|
from PIL import Image |
|
from PIL import ImageFont |
|
from PIL import ImageDraw |
|
import cv2 |
|
|
|
from utils import get_files, load_image |
|
from skimage import io |
|
|
|
|
|
class PanelExtractor: |
|
def __init__(self, min_pct_panel=2, max_pct_panel=90, paper_th=0.35): |
|
assert min_pct_panel < max_pct_panel, "Minimum percentage must be smaller than maximum percentage" |
|
self.min_panel = min_pct_panel / 100 |
|
self.max_panel = max_pct_panel / 100 |
|
self.paper_th = paper_th |
|
|
|
def _generate_panel_blocks(self, img): |
|
img = img if len(img.shape) == 2 else img[:, :, 0] |
|
blur = cv2.GaussianBlur(img, (5, 5), 0) |
|
thresh = cv2.threshold(blur, 230, 255, cv2.THRESH_BINARY)[1] |
|
cv2.rectangle(thresh, (0, 0), tuple(img.shape[::-1]), (0, 0, 0), 10) |
|
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(thresh, 4, cv2.CV_32S) |
|
ind = np.argsort(stats[:, 4], )[::-1][1] |
|
panel_block_mask = ((labels == ind) * 255).astype("uint8") |
|
|
|
return panel_block_mask |
|
|
|
def generate_panels(self, img): |
|
block_mask = self._generate_panel_blocks(img) |
|
cv2.rectangle(block_mask, (0, 0), tuple(block_mask.shape[::-1]), (255, 255, 255), 10) |
|
|
|
|
|
|
|
contours, hierarchy = cv2.findContours(block_mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) |
|
panels = [] |
|
masks = [] |
|
panel_masks = [] |
|
|
|
|
|
for i in range(len(contours)): |
|
area = cv2.contourArea(contours[i]) |
|
img_area = img.shape[0] * img.shape[1] |
|
|
|
|
|
if area < (self.min_panel * img_area) or area > (self.max_panel * img_area): |
|
continue |
|
|
|
x, y, w, h = cv2.boundingRect(contours[i]) |
|
masks.append(cv2.boundingRect(contours[i])) |
|
|
|
panel_mask = np.ones_like(block_mask, "int32") |
|
cv2.fillPoly(panel_mask, [contours[i].astype("int32")], color=(0, 0, 0)) |
|
|
|
panel_mask = panel_mask[y:y + h, x:x + w].copy() |
|
|
|
|
|
|
|
panel = img[y:y + h, x:x + w].copy() |
|
|
|
panel[panel_mask == 1] = 255 |
|
|
|
|
|
panels.append(panel) |
|
panel_masks.append(panel_mask) |
|
|
|
return panels, masks, panel_masks |
|
|
|
def extract(self, folder): |
|
print("Loading images ... ", end="") |
|
|
|
image_list = [] |
|
image_list.append(folder) |
|
imgs = [load_image(x) for x in image_list] |
|
print("Done!") |
|
|
|
folder = os.path.dirname(folder) |
|
|
|
if not exists(join(folder, "panels")): |
|
makedirs(join(folder, "panels")) |
|
folder = join(folder, "panels") |
|
|
|
|
|
paperless_imgs = [] |
|
for img in tqdm(imgs, desc="Removing images with paper texture"): |
|
hist, bins = np.histogram(img.copy().ravel(), 256, [0, 256]) |
|
if np.sum(hist[50:200]) / np.sum(hist) < self.paper_th: |
|
paperless_imgs.append(img) |
|
|
|
if not paperless_imgs: |
|
return imgs, [], [] |
|
for i, img in tqdm(enumerate(paperless_imgs), desc="extracting panels"): |
|
panels, masks, panel_masks = self.generate_panels(img) |
|
name, ext = splitext(basename(image_list[i])) |
|
for j, panel in enumerate(panels): |
|
cv2.imwrite(join(folder, f'{name}_{j}.{ext}'), panel) |
|
|
|
|
|
img = Image.fromarray(img) |
|
draw = ImageDraw.Draw(img) |
|
font = ImageFont.truetype('extractor/Open-Sans-Bold.ttf', 160) |
|
|
|
def flatten(l): |
|
for el in l: |
|
if isinstance(el, list): |
|
yield from flatten(el) |
|
else: |
|
yield el |
|
|
|
for i, bbox in enumerate(flatten(masks), start=1): |
|
w, h = draw.textsize(str(i), font=font) |
|
y = (bbox[1] + bbox[3] / 2 - h / 2) |
|
x = (bbox[0] + bbox[2] / 2 - w / 2) |
|
draw.text((x, y), str(i), (255, 215, 0), font=font) |
|
img.show() |
|
return panels, masks, panel_masks |
|
|
|
def concatPanels(self, img_file, fake_imgs, masks, panel_masks): |
|
img = io.imread(img_file) |
|
|
|
|
|
|
|
for i in range(len(fake_imgs)): |
|
x, y, w, h = masks[i] |
|
|
|
|
|
fake_img = fake_imgs[i] |
|
panel_mask = panel_masks[i] |
|
img[y:y + h, x:x + w][panel_mask == 0] = fake_img[panel_mask == 0] |
|
|
|
out_folder = os.path.dirname(img_file) |
|
out_name = os.path.basename(img_file) |
|
out_name = os.path.splitext(out_name)[0] |
|
out_img_path = os.path.join(out_folder,'color',f'{out_name}_color.png') |
|
|
|
|
|
Image.fromarray(img).show() |
|
|
|
folder_path = os.path.join(out_folder, 'color') |
|
if not os.path.exists(folder_path): |
|
os.mkdir(folder_path) |
|
io.imsave(out_img_path, img) |
|
|
|
|
|
def main(args): |
|
panel_extractor = PanelExtractor(min_pct_panel=args.min_panel, max_pct_panel=args.max_panel) |
|
panels, masks, panel_masks = panel_extractor.extract(args.folder) |
|
panel_extractor.concatPanels(args.folder, [], masks, panel_masks) |
|
|
|
|
|
if __name__ == "__main__": |
|
parser = argparse.ArgumentParser( |
|
description="Implementation of a Manga Panel Extractor and dialogue bubble text eraser.", |
|
formatter_class=RawTextHelpFormatter |
|
) |
|
parser.add_argument("-minp", "--min_panel", type=int, choices=range(1, 99), default=5, metavar="[1-99]", |
|
help="Percentage of minimum panel area in relation to total page area.") |
|
parser.add_argument("-maxp", "--max_panel", type=int, choices=range(1, 99), default=90, metavar="[1-99]", |
|
help="Percentage of minimum panel area in relation to total page area.") |
|
parser.add_argument("-f", '--folder', default='./images/002.png', type=str, |
|
help="""folder path to input manga pages. |
|
Panels will be saved to a directory named `panels` in this folder.""") |
|
|
|
args = parser.parse_args() |
|
main(args) |
|
|