|
""" |
|
adenzu Eren Manga/Comics Panel Extractor (WebUI) |
|
Copyright (C) 2025 avan |
|
|
|
This program is a web interface for the adenzu library. |
|
The core logic is based on adenzu Eren, the Manga Panel Extractor. |
|
Copyright (c) 2023 Eren |
|
""" |
|
import gradio as gr |
|
import os |
|
import cv2 |
|
import numpy as np |
|
import tempfile |
|
import shutil |
|
from tqdm import tqdm |
|
|
|
from image_processing.panel import generate_panel_blocks, generate_panel_blocks_by_ai |
|
from manga_panel_processor import remove_border |
|
|
|
|
|
DESCRIPTION = """ |
|
# adenzu Eren Manga/Comics Panel Extractor (WebUI) |
|
Upload your manga or comic book images. This tool will automatically analyze and extract each panel. |
|
You can choose between a Traditional algorithm or a AI-based model for processing. |
|
Finally, all extracted panels are packaged into a single ZIP file for you to download. |
|
|
|
The Core package author: **adenzu Eren** ([Original Project](https://github.com/adenzu/Manga-Panel-Extractor)). |
|
""" |
|
|
|
def process_images( |
|
input_files, |
|
method, |
|
separate_folders, |
|
rtl_order, |
|
remove_borders, |
|
|
|
merge_mode, |
|
split_joint, |
|
fallback, |
|
output_mode, |
|
|
|
|
|
progress=gr.Progress(track_tqdm=True) |
|
): |
|
""" |
|
Main processing function called by Gradio. |
|
It takes uploaded files and settings, processes them, and returns a zip file. |
|
""" |
|
if not input_files: |
|
raise gr.Error("No images uploaded. Please upload at least one image.") |
|
|
|
|
|
with tempfile.TemporaryDirectory() as temp_panel_dir: |
|
print(f"Created temporary directory for panels: {temp_panel_dir}") |
|
|
|
for image_file in tqdm(input_files, desc="Processing Images"): |
|
try: |
|
|
|
original_filename = os.path.basename(image_file.name) |
|
filename_no_ext, file_ext = os.path.splitext(original_filename) |
|
|
|
|
|
image = cv2.imread(image_file.name) |
|
if image is None: |
|
print(f"Warning: Could not read image {original_filename}. Skipping.") |
|
continue |
|
|
|
|
|
if method == "Traditional": |
|
panel_blocks = generate_panel_blocks( |
|
image=image, |
|
split_joint_panels=split_joint, |
|
fallback=fallback, |
|
mode=output_mode, |
|
merge=merge_mode, |
|
rtl_order=rtl_order |
|
) |
|
elif method == "AI": |
|
panel_blocks = generate_panel_blocks_by_ai( |
|
image=image, |
|
merge=merge_mode, |
|
rtl_order=rtl_order |
|
) |
|
else: |
|
|
|
panel_blocks = [] |
|
|
|
if not panel_blocks: |
|
print(f"Warning: No panels found in {original_filename}.") |
|
continue |
|
|
|
|
|
if separate_folders: |
|
|
|
image_output_folder = os.path.join(temp_panel_dir, filename_no_ext) |
|
os.makedirs(image_output_folder, exist_ok=True) |
|
else: |
|
|
|
image_output_folder = temp_panel_dir |
|
|
|
|
|
for i, panel in enumerate(panel_blocks): |
|
if remove_borders: |
|
panel = remove_border(panel) |
|
if separate_folders: |
|
|
|
panel_filename = f"panel_{i}{file_ext if file_ext else '.png'}" |
|
else: |
|
|
|
panel_filename = f"{filename_no_ext}_panel_{i}{file_ext if file_ext else '.png'}" |
|
|
|
output_path = os.path.join(image_output_folder, panel_filename) |
|
cv2.imwrite(output_path, panel) |
|
|
|
except Exception as e: |
|
print(f"Error processing {original_filename}: {e}") |
|
raise gr.Error(f"Failed to process {original_filename}: {e}") |
|
|
|
|
|
if not os.listdir(temp_panel_dir): |
|
raise gr.Error("Processing complete, but no panels were extracted from any of the images.") |
|
|
|
|
|
|
|
|
|
|
|
zip_output_dir = tempfile.mkdtemp() |
|
|
|
|
|
zip_path_base = os.path.join(zip_output_dir, "adenzu_output") |
|
|
|
|
|
|
|
|
|
final_zip_path = shutil.make_archive( |
|
base_name=zip_path_base, |
|
format='zip', |
|
root_dir=temp_panel_dir |
|
) |
|
|
|
print(f"Created ZIP file at: {final_zip_path}") |
|
|
|
|
|
return final_zip_path |
|
|
|
|
|
def main(): |
|
""" |
|
Defines and launches the Gradio interface. |
|
""" |
|
with gr.Blocks(theme=gr.themes.Soft()) as demo: |
|
gr.Markdown(DESCRIPTION) |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=1): |
|
|
|
input_files = gr.Files( |
|
label="Upload Manga Pages", |
|
file_types=["image"], |
|
file_count="multiple" |
|
) |
|
|
|
method = gr.Radio( |
|
label="Processing Method", |
|
choices=["Traditional", "AI"], |
|
value="Traditional", |
|
interactive=True |
|
) |
|
|
|
|
|
gr.Markdown("### Output Options") |
|
separate_folders = gr.Checkbox( |
|
label="Create a separate folder for each image inside the ZIP", |
|
value=True, |
|
info="If unchecked, all panels will be in the root of the ZIP, with filenames prefixed by the original image name." |
|
) |
|
|
|
rtl_order = gr.Checkbox( |
|
label="Right-to-Left (RTL) Reading Order", |
|
value=True, |
|
info="Check this for manga that is read from right to left. Uncheck for western comics." |
|
) |
|
|
|
remove_borders = gr.Checkbox( |
|
label="Attempt to remove panel borders", |
|
value=False, |
|
info="Crops the image to the content area. May not be perfect for all images." |
|
) |
|
|
|
|
|
gr.Markdown("### Shared Parameters") |
|
merge_mode = gr.Dropdown( |
|
label="Merge Mode", |
|
choices=['none', 'vertical', 'horizontal'], |
|
value='none', |
|
info="How to merge detected panels before saving." |
|
) |
|
|
|
|
|
with gr.Group(visible=True) as traditional_params: |
|
gr.Markdown("### Traditional Method Parameters") |
|
split_joint = gr.Checkbox( |
|
label="Split Joint Panels", |
|
value=False, |
|
info="For panels that are touching or share a single border line. This algorithm actively tries to draw a separation line between them. Useful if multiple panels are being detected as one large block, but may occasionally split a single large panel by mistake." |
|
) |
|
fallback = gr.Checkbox( |
|
label="Fallback to Threshold Extraction", |
|
value=True, |
|
info="If the main algorithm fails to find multiple panels (e.g., on a borderless page or a full-bleed splash page), this enables a secondary, simpler extraction method. It's a 'safety net' that can find panels when the primary method cannot." |
|
) |
|
output_mode = gr.Dropdown( |
|
label="Output Mode", |
|
choices=['bounding', 'masked'], |
|
value='bounding', |
|
info="bounding: Crops a rectangular area around each panel. Best for general use. \nmasked: Crops along the exact, non-rectangular shape of the panel, filling the outside with a background color. Best for irregularly shaped panels." |
|
) |
|
|
|
with gr.Group(visible=False) as ai_params: |
|
gr.Markdown("### AI Method Parameters") |
|
gr.Markdown("_(Currently, only the shared 'Merge Mode' parameter is used by the AI method.)_") |
|
|
|
|
|
def toggle_parameter_visibility(selected_method): |
|
if selected_method == "Traditional": |
|
return gr.update(visible=True), gr.update(visible=False) |
|
elif selected_method == "AI": |
|
return gr.update(visible=False), gr.update(visible=True) |
|
|
|
method.change( |
|
fn=toggle_parameter_visibility, |
|
inputs=method, |
|
outputs=[traditional_params, ai_params] |
|
) |
|
|
|
|
|
generate_button = gr.Button("Generate Panels", variant="primary") |
|
|
|
with gr.Column(scale=1): |
|
|
|
output_zip = gr.File(label="Download ZIP") |
|
|
|
|
|
generate_button.click( |
|
fn=process_images, |
|
inputs=[ |
|
input_files, |
|
method, |
|
separate_folders, |
|
rtl_order, |
|
remove_borders, |
|
merge_mode, |
|
split_joint, |
|
fallback, |
|
output_mode |
|
], |
|
outputs=output_zip |
|
) |
|
|
|
demo.launch(inbrowser=True) |
|
|
|
if __name__ == "__main__": |
|
main() |