File size: 13,558 Bytes
df7cb52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
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("""

    <div class="header">

        <h1>🎨 MANGA SPLITTER PRO</h1>

        <p>Công cụ cắt ảnh manga thông minh - Tự động phát hiện vùng trắng & đen</p>

        <p>Tránh cắt trùng vào bóng thoại • Hỗ trợ OCR tối ưu</p>

    </div>

    """)
    
    with gr.Row():
        with gr.Column(scale=1):
            gr.HTML("<h3>📤 Upload & Cài đặt</h3>")
            
            # Upload file
            image_input = gr.File(
                label="🖼️ Chọn ảnh manga",
                file_types=[".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tiff"],
                type="filepath"
            )
            
            # Cài đặt
            with gr.Accordion("⚙️ Cài đặt nâng cao", open=False):
                auto_height = gr.Checkbox(
                    label="🤖 Tự động điều chỉnh chiều cao tối ưu",
                    value=True,
                    info="Tự động tính chiều cao phù hợp dựa trên kích thước ảnh"
                )
                
                max_height = gr.Slider(
                    minimum=500, maximum=5000, value=2000, step=100,
                    label="📏 Chiều cao tối đa mỗi phần (px)",
                    visible=False
                )
                
                white_threshold = gr.Slider(
                    minimum=200, maximum=255, value=240, step=5,
                    label="⚪ Ngưỡng pixel trắng"
                )
                
                black_threshold = gr.Slider(
                    minimum=0, maximum=50, value=15, step=5,
                    label="⚫ Ngưỡng pixel đen"
                )
                
                min_separator_height = gr.Slider(
                    minimum=5, maximum=50, value=15, step=5,
                    label="📐 Chiều cao tối thiểu vùng separator"
                )
                
                show_preview = gr.Checkbox(
                    label="👁️ Hiển thị preview điểm cắt",
                    value=True
                )
            
            # Nút xử lý
            process_btn = gr.Button(
                "🚀 Cắt ảnh manga", 
                variant="primary",
                size="lg"
            )
        
        with gr.Column(scale=1):
            gr.HTML("<h3>📊 Kết quả</h3>")
            
            # Kết quả
            result_text = gr.Markdown(
                value="👋 Chào mừng! Upload ảnh manga để bắt đầu cắt.",
                label="📈 Thông tin"
            )
            
            # Download
            download_file = gr.File(
                label="💾 Tải xuống file ZIP",
                visible=False
            )
            
            # Preview
            preview_image = gr.Image(
                label="👁️ Preview điểm cắt",
                visible=False
            )
    
    # Thông tin hướng dẫn
    with gr.Accordion("📚 Hướng dẫn sử dụng", open=False):
        gr.Markdown("""

        ### 🎯 Cách sử dụng:

        1. **Upload ảnh** manga (JPG, PNG) vào khung bên trái

        2. **Điều chỉnh cài đặt** nếu cần (mặc định đã tối ưu)

        3. **Bấm "Cắt ảnh manga"** để xử lý

        4. **Tải xuống** file ZIP chứa các phần đã cắt

        

        ### ⚙️ Tham số:

        - **Chiều cao tối đa**: Giới hạn chiều cao mỗi phần cắt

        - **Ngưỡng trắng**: Độ trắng để nhận diện vùng separator (240 = rất trắng)

        - **Ngưỡng đen**: Độ đen để nhận diện vùng dramatic (15 = rất đen) 

        - **Chiều cao separator**: Vùng separator phải cao ít nhất bao nhiêu pixel

        

        ### 🎨 Hoạt động với:

        - ✅ Manga truyền thống (nền trắng)

        - ✅ Action manga (vùng đen dramatic)

        - ✅ Webtoon dài

        - ✅ Raw scan chất lượng cao

        """)
    
    # Xử lý sự kiện
    def toggle_manual_height(auto_mode):
        return gr.update(visible=not auto_mode, value=2000)
    
    auto_height.change(
        toggle_manual_height,
        inputs=[auto_height],
        outputs=[max_height]
    )
    
    process_btn.click(
        fn=process_manga,
        inputs=[image_input, auto_height, max_height, white_threshold, black_threshold, min_separator_height, show_preview],
        outputs=[result_text, download_file, preview_image]
    ).then(
        # Hiển thị kết quả
        lambda zip_file, preview_img: (
            gr.update(visible=zip_file is not None), 
            gr.update(visible=preview_img is not None)
        ),
        inputs=[download_file, preview_image],
        outputs=[download_file, preview_image]
    )

if __name__ == "__main__":
    app.launch(
        server_name="0.0.0.0",  # Cho phép truy cập từ mạng local
        server_port=7860,
        share=False,  # Đặt True nếu muốn public link
        debug=True
    )