haaaaus commited on
Commit
df7cb52
·
verified ·
1 Parent(s): de9951c

Upload 2 files

Browse files
Files changed (2) hide show
  1. manga_web.py +351 -0
  2. requirements.txt +4 -0
manga_web.py ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import cv2
3
+ import numpy as np
4
+ import os
5
+ import zipfile
6
+ from pathlib import Path
7
+ import tempfile
8
+ import shutil
9
+ from typing import List, Tuple, Optional
10
+
11
+ class MangaWebSplitter:
12
+ def __init__(self):
13
+ self.temp_dir = None
14
+
15
+ def detect_separators(self, image: np.ndarray,
16
+ white_threshold: int = 240,
17
+ black_threshold: int = 15,
18
+ min_separator_height: int = 15) -> List[int]:
19
+ """
20
+ Phát hiện các vùng separator (trắng hoặc đen) để cắt
21
+ """
22
+ if len(image.shape) == 3:
23
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
24
+ else:
25
+ gray = image.copy()
26
+
27
+ height, width = gray.shape
28
+ split_points = []
29
+
30
+ # Phân tích từng hàng pixel (kiểm tra mỗi 3 pixel để tăng tốc)
31
+ for y in range(0, height, 3):
32
+ row = gray[y, :]
33
+
34
+ # Đếm pixel trắng và đen
35
+ white_pixels = np.sum(row > white_threshold)
36
+ black_pixels = np.sum(row < black_threshold)
37
+ white_ratio = white_pixels / width
38
+ black_ratio = black_pixels / width
39
+
40
+ # Kiểm tra xem có phải là vùng separator không
41
+ is_separator = white_ratio > 0.85 or black_ratio > 0.85
42
+
43
+ if is_separator:
44
+ # Kiểm tra chiều cao vùng separator
45
+ separator_height = 0
46
+ separator_type = "white" if white_ratio > black_ratio else "black"
47
+
48
+ for check_y in range(y, min(y + 50, height)):
49
+ check_row = gray[check_y, :]
50
+ if separator_type == "white":
51
+ check_ratio = np.sum(check_row > white_threshold) / width
52
+ else:
53
+ check_ratio = np.sum(check_row < black_threshold) / width
54
+
55
+ if check_ratio > 0.85:
56
+ separator_height += 1
57
+ else:
58
+ break
59
+
60
+ # Nếu vùng separator đủ cao
61
+ if separator_height >= min_separator_height:
62
+ split_points.append(y + separator_height // 2)
63
+
64
+ # Loại bỏ các điểm quá gần nhau
65
+ filtered_points = []
66
+ for point in split_points:
67
+ if not filtered_points or point - filtered_points[-1] > 100:
68
+ filtered_points.append(point)
69
+
70
+ return filtered_points
71
+
72
+ def split_manga(self,
73
+ image_file,
74
+ max_height: int = None,
75
+ white_threshold: int = 240,
76
+ black_threshold: int = 15,
77
+ min_separator_height: int = 15,
78
+ show_preview: bool = False,
79
+ auto_height: bool = True) -> Tuple[str, str, str]:
80
+ """
81
+ Cắt ảnh manga và trả về kết quả
82
+ """
83
+ try:
84
+ # Đọc ảnh
85
+ image = cv2.imread(image_file)
86
+ if image is None:
87
+ return "❌ Không thể đọc file ảnh! Vui lòng kiểm tra định dạng.", "", ""
88
+
89
+ height, width = image.shape[:2]
90
+
91
+ # Tự động điều chỉnh chiều cao nếu bật auto mode
92
+ if auto_height or max_height is None or max_height == 0:
93
+ if height <= 2000:
94
+ calculated_height = max(height // 2, 800)
95
+ elif height <= 4000:
96
+ calculated_height = max(height // 3, 1000)
97
+ elif height <= 6000:
98
+ calculated_height = 2000
99
+ elif height <= 10000:
100
+ calculated_height = 2500
101
+ else:
102
+ calculated_height = height // (height // 2000)
103
+
104
+ max_height = calculated_height
105
+ auto_mode_text = f" (Tự động tối ưu)"
106
+ else:
107
+ auto_mode_text = " (Thủ công)"
108
+
109
+ # Tìm điểm cắt
110
+ split_points = self.detect_separators(
111
+ image, white_threshold, black_threshold, min_separator_height
112
+ )
113
+
114
+ # Nếu không tìm thấy điểm cắt, dùng chiều cao đã tính
115
+ if not split_points:
116
+ split_points = list(range(max_height, height, max_height))
117
+ info_msg = f"🔄 Không tìm thấy vùng separator, cắt theo chiều cao {max_height}px{auto_mode_text}"
118
+ else:
119
+ info_msg = f"✅ Tìm thấy {len(split_points)} vùng separator tự động{auto_mode_text}"
120
+
121
+ # Thêm điểm đầu và cuối
122
+ all_points = [0] + split_points + [height]
123
+ all_points = sorted(list(set(all_points)))
124
+
125
+ # Tạo thư mục tạm
126
+ self.temp_dir = tempfile.mkdtemp()
127
+ output_files = []
128
+ base_name = Path(image_file).stem
129
+
130
+ # Cắt ảnh
131
+ valid_parts = 0
132
+ for i in range(len(all_points) - 1):
133
+ start_y = all_points[i]
134
+ end_y = all_points[i + 1]
135
+
136
+ # Bỏ qua phần quá nhỏ
137
+ if end_y - start_y < 100:
138
+ continue
139
+
140
+ valid_parts += 1
141
+ cropped = image[start_y:end_y, :]
142
+
143
+ # Lưu file
144
+ output_file = os.path.join(self.temp_dir, f"{base_name}_part_{valid_parts:03d}.png")
145
+ cv2.imwrite(output_file, cropped)
146
+ output_files.append(output_file)
147
+
148
+ # Tạo file zip
149
+ zip_path = os.path.join(self.temp_dir, f"{base_name}_split.zip")
150
+ with zipfile.ZipFile(zip_path, 'w') as zipf:
151
+ for file in output_files:
152
+ zipf.write(file, os.path.basename(file))
153
+
154
+ result_msg = f"""
155
+ 📊 **Kết quả cắt ảnh:**
156
+ - Kích thước gốc: {width}×{height}px
157
+ - Chiều cao mỗi phần: {max_height}px{auto_mode_text}
158
+ - Số phần đã cắt: {valid_parts}
159
+ - Điểm cắt: {split_points if split_points else 'Theo chiều cao cố định'}
160
+
161
+ {info_msg}
162
+ """.strip()
163
+
164
+ # Tạo preview nếu yêu cầu
165
+ preview_path = ""
166
+ if show_preview and split_points:
167
+ preview_image = image.copy()
168
+ for point in split_points:
169
+ cv2.line(preview_image, (0, point), (width, point), (0, 0, 255), 3)
170
+ preview_path = os.path.join(self.temp_dir, "preview.png")
171
+ cv2.imwrite(preview_path, preview_image)
172
+
173
+ return result_msg, zip_path, preview_path
174
+
175
+ except Exception as e:
176
+ return f"❌ Lỗi: {str(e)}", "", ""
177
+
178
+ # Khởi tạo splitter
179
+ splitter = MangaWebSplitter()
180
+
181
+ def process_manga(image_file, auto_height, max_height, white_threshold, black_threshold, min_separator_height, show_preview):
182
+ """Xử lý ảnh manga"""
183
+ if image_file is None:
184
+ return "⚠️ Vui lòng upload ảnh trước!", None, None
185
+
186
+ result_msg, zip_path, preview_path = splitter.split_manga(
187
+ image_file, max_height, white_threshold, black_threshold, min_separator_height, show_preview, auto_height
188
+ )
189
+
190
+ return result_msg, zip_path if zip_path else None, preview_path if preview_path else None
191
+
192
+ # CSS tùy chỉnh
193
+ css = """
194
+ .container {
195
+ max-width: 1200px;
196
+ margin: 0 auto;
197
+ }
198
+ .header {
199
+ text-align: center;
200
+ background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
201
+ color: white;
202
+ padding: 2rem;
203
+ border-radius: 10px;
204
+ margin-bottom: 2rem;
205
+ }
206
+ .upload-area {
207
+ border: 2px dashed #667eea;
208
+ border-radius: 10px;
209
+ padding: 2rem;
210
+ text-align: center;
211
+ background: #f8f9ff;
212
+ }
213
+ """
214
+
215
+ # Tạo giao diện Gradio
216
+ with gr.Blocks(css=css, title="🎨 Manga Splitter Pro") as app:
217
+
218
+ gr.HTML("""
219
+ <div class="header">
220
+ <h1>🎨 MANGA SPLITTER PRO</h1>
221
+ <p>Công cụ cắt ảnh manga thông minh - Tự động phát hiện vùng trắng & đen</p>
222
+ <p>Tránh cắt trùng vào bóng thoại • Hỗ trợ OCR tối ưu</p>
223
+ </div>
224
+ """)
225
+
226
+ with gr.Row():
227
+ with gr.Column(scale=1):
228
+ gr.HTML("<h3>📤 Upload & Cài đặt</h3>")
229
+
230
+ # Upload file
231
+ image_input = gr.File(
232
+ label="🖼️ Chọn ảnh manga",
233
+ file_types=[".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tiff"],
234
+ type="filepath"
235
+ )
236
+
237
+ # Cài đặt
238
+ with gr.Accordion("⚙️ Cài đặt nâng cao", open=False):
239
+ auto_height = gr.Checkbox(
240
+ label="🤖 Tự động điều chỉnh chiều cao tối ưu",
241
+ value=True,
242
+ info="Tự động tính chiều cao phù hợp dựa trên kích thước ảnh"
243
+ )
244
+
245
+ max_height = gr.Slider(
246
+ minimum=500, maximum=5000, value=2000, step=100,
247
+ label="📏 Chiều cao tối đa mỗi phần (px)",
248
+ visible=False
249
+ )
250
+
251
+ white_threshold = gr.Slider(
252
+ minimum=200, maximum=255, value=240, step=5,
253
+ label="⚪ Ngưỡng pixel trắng"
254
+ )
255
+
256
+ black_threshold = gr.Slider(
257
+ minimum=0, maximum=50, value=15, step=5,
258
+ label="⚫ Ngưỡng pixel đen"
259
+ )
260
+
261
+ min_separator_height = gr.Slider(
262
+ minimum=5, maximum=50, value=15, step=5,
263
+ label="📐 Chiều cao tối thiểu vùng separator"
264
+ )
265
+
266
+ show_preview = gr.Checkbox(
267
+ label="👁️ Hiển thị preview điểm cắt",
268
+ value=True
269
+ )
270
+
271
+ # Nút xử lý
272
+ process_btn = gr.Button(
273
+ "🚀 Cắt ảnh manga",
274
+ variant="primary",
275
+ size="lg"
276
+ )
277
+
278
+ with gr.Column(scale=1):
279
+ gr.HTML("<h3>📊 Kết quả</h3>")
280
+
281
+ # Kết quả
282
+ result_text = gr.Markdown(
283
+ value="👋 Chào mừng! Upload ảnh manga để bắt đầu cắt.",
284
+ label="📈 Thông tin"
285
+ )
286
+
287
+ # Download
288
+ download_file = gr.File(
289
+ label="💾 Tải xuống file ZIP",
290
+ visible=False
291
+ )
292
+
293
+ # Preview
294
+ preview_image = gr.Image(
295
+ label="👁️ Preview điểm cắt",
296
+ visible=False
297
+ )
298
+
299
+ # Thông tin hướng dẫn
300
+ with gr.Accordion("📚 Hướng dẫn sử dụng", open=False):
301
+ gr.Markdown("""
302
+ ### 🎯 Cách sử dụng:
303
+ 1. **Upload ảnh** manga (JPG, PNG) vào khung bên trái
304
+ 2. **Điều chỉnh cài đặt** nếu cần (mặc định đã tối ưu)
305
+ 3. **Bấm "Cắt ảnh manga"** để xử lý
306
+ 4. **Tải xuống** file ZIP chứa các phần đã cắt
307
+
308
+ ### ⚙️ Tham số:
309
+ - **Chiều cao tối đa**: Giới hạn chiều cao mỗi phần cắt
310
+ - **Ngưỡng trắng**: Độ trắng để nhận diện vùng separator (240 = rất trắng)
311
+ - **Ngưỡng đen**: Độ đen để nhận diện vùng dramatic (15 = rất đen)
312
+ - **Chiều cao separator**: Vùng separator phải cao ít nhất bao nhiêu pixel
313
+
314
+ ### 🎨 Hoạt động với:
315
+ - ✅ Manga truyền thống (nền trắng)
316
+ - ✅ Action manga (vùng đen dramatic)
317
+ - ✅ Webtoon dài
318
+ - ✅ Raw scan chất lượng cao
319
+ """)
320
+
321
+ # Xử lý sự kiện
322
+ def toggle_manual_height(auto_mode):
323
+ return gr.update(visible=not auto_mode, value=2000)
324
+
325
+ auto_height.change(
326
+ toggle_manual_height,
327
+ inputs=[auto_height],
328
+ outputs=[max_height]
329
+ )
330
+
331
+ process_btn.click(
332
+ fn=process_manga,
333
+ inputs=[image_input, auto_height, max_height, white_threshold, black_threshold, min_separator_height, show_preview],
334
+ outputs=[result_text, download_file, preview_image]
335
+ ).then(
336
+ # Hiển thị kết quả
337
+ lambda zip_file, preview_img: (
338
+ gr.update(visible=zip_file is not None),
339
+ gr.update(visible=preview_img is not None)
340
+ ),
341
+ inputs=[download_file, preview_image],
342
+ outputs=[download_file, preview_image]
343
+ )
344
+
345
+ if __name__ == "__main__":
346
+ app.launch(
347
+ server_name="0.0.0.0", # Cho phép truy cập từ mạng local
348
+ server_port=7860,
349
+ share=False, # Đặt True nếu muốn public link
350
+ debug=True
351
+ )
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ opencv-python>=4.5.0
2
+ numpy>=1.19.0
3
+ pathlib
4
+ gradio>=4.0.0