import gradio as gr import cv2 import numpy as np import os import zipfile from pathlib import Path import tempfile import shutil from typing import List, Tuple, Optional class MangaWebSplitter: def __init__(self): self.temp_dir = None def detect_separators(self, image: np.ndarray, white_threshold: int = 240, black_threshold: int = 15, min_separator_height: int = 15) -> List[int]: """ Phát hiện các vùng separator (trắng hoặc đen) để cắt """ if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) else: gray = image.copy() height, width = gray.shape split_points = [] # Phân tích từng hàng pixel (kiểm tra mỗi 3 pixel để tăng tốc) for y in range(0, height, 3): row = gray[y, :] # Đếm pixel trắng và đen white_pixels = np.sum(row > white_threshold) black_pixels = np.sum(row < black_threshold) white_ratio = white_pixels / width black_ratio = black_pixels / width # Kiểm tra xem có phải là vùng separator không is_separator = white_ratio > 0.85 or black_ratio > 0.85 if is_separator: # Kiểm tra chiều cao vùng separator separator_height = 0 separator_type = "white" if white_ratio > black_ratio else "black" for check_y in range(y, min(y + 50, height)): check_row = gray[check_y, :] if separator_type == "white": check_ratio = np.sum(check_row > white_threshold) / width else: check_ratio = np.sum(check_row < black_threshold) / width if check_ratio > 0.85: separator_height += 1 else: break # Nếu vùng separator đủ cao if separator_height >= min_separator_height: split_points.append(y + separator_height // 2) # Loại bỏ các điểm quá gần nhau filtered_points = [] for point in split_points: if not filtered_points or point - filtered_points[-1] > 100: filtered_points.append(point) return filtered_points def split_manga(self, image_file, max_height: int = None, white_threshold: int = 240, black_threshold: int = 15, min_separator_height: int = 15, show_preview: bool = False, auto_height: bool = True) -> Tuple[str, str, str]: """ Cắt ảnh manga và trả về kết quả """ try: # Đọc ảnh image = cv2.imread(image_file) if image is None: return "❌ Không thể đọc file ảnh! Vui lòng kiểm tra định dạng.", "", "" height, width = image.shape[:2] # Tự động điều chỉnh chiều cao nếu bật auto mode if auto_height or max_height is None or max_height == 0: if height <= 2000: calculated_height = max(height // 2, 800) elif height <= 4000: calculated_height = max(height // 3, 1000) elif height <= 6000: calculated_height = 2000 elif height <= 10000: calculated_height = 2500 else: calculated_height = height // (height // 2000) max_height = calculated_height auto_mode_text = f" (Tự động tối ưu)" else: auto_mode_text = " (Thủ công)" # Tìm điểm cắt split_points = self.detect_separators( image, white_threshold, black_threshold, min_separator_height ) # Nếu không tìm thấy điểm cắt, dùng chiều cao đã tính if not split_points: split_points = list(range(max_height, height, max_height)) info_msg = f"🔄 Không tìm thấy vùng separator, cắt theo chiều cao {max_height}px{auto_mode_text}" else: info_msg = f"✅ Tìm thấy {len(split_points)} vùng separator tự động{auto_mode_text}" # Thêm điểm đầu và cuối all_points = [0] + split_points + [height] all_points = sorted(list(set(all_points))) # Tạo thư mục tạm self.temp_dir = tempfile.mkdtemp() output_files = [] base_name = Path(image_file).stem # Cắt ảnh valid_parts = 0 for i in range(len(all_points) - 1): start_y = all_points[i] end_y = all_points[i + 1] # Bỏ qua phần quá nhỏ if end_y - start_y < 100: continue valid_parts += 1 cropped = image[start_y:end_y, :] # Lưu file output_file = os.path.join(self.temp_dir, f"{base_name}_part_{valid_parts:03d}.png") cv2.imwrite(output_file, cropped) output_files.append(output_file) # Tạo file zip zip_path = os.path.join(self.temp_dir, f"{base_name}_split.zip") with zipfile.ZipFile(zip_path, 'w') as zipf: for file in output_files: zipf.write(file, os.path.basename(file)) result_msg = f""" 📊 **Kết quả cắt ảnh:** - Kích thước gốc: {width}×{height}px - Chiều cao mỗi phần: {max_height}px{auto_mode_text} - Số phần đã cắt: {valid_parts} - Điểm cắt: {split_points if split_points else 'Theo chiều cao cố định'} {info_msg} """.strip() # Tạo preview nếu yêu cầu preview_path = "" if show_preview and split_points: preview_image = image.copy() for point in split_points: cv2.line(preview_image, (0, point), (width, point), (0, 0, 255), 3) preview_path = os.path.join(self.temp_dir, "preview.png") cv2.imwrite(preview_path, preview_image) return result_msg, zip_path, preview_path except Exception as e: return f"❌ Lỗi: {str(e)}", "", "" # Khởi tạo splitter splitter = MangaWebSplitter() def process_manga(image_file, auto_height, max_height, white_threshold, black_threshold, min_separator_height, show_preview): """Xử lý ảnh manga""" if image_file is None: return "⚠️ Vui lòng upload ảnh trước!", None, None result_msg, zip_path, preview_path = splitter.split_manga( image_file, max_height, white_threshold, black_threshold, min_separator_height, show_preview, auto_height ) return result_msg, zip_path if zip_path else None, preview_path if preview_path else None # CSS tùy chỉnh css = """ .container { max-width: 1200px; margin: 0 auto; } .header { text-align: center; background: linear-gradient(45deg, #667eea 0%, #764ba2 100%); color: white; padding: 2rem; border-radius: 10px; margin-bottom: 2rem; } .upload-area { border: 2px dashed #667eea; border-radius: 10px; padding: 2rem; text-align: center; background: #f8f9ff; } """ # Tạo giao diện Gradio with gr.Blocks(css=css, title="🎨 Manga Splitter Pro") as app: gr.HTML("""
Công cụ cắt ảnh manga thông minh - Tự động phát hiện vùng trắng & đen
Tránh cắt trùng vào bóng thoại • Hỗ trợ OCR tối ưu