DucHaiten commited on
Commit
a3fda21
1 Parent(s): 5df4df8

Upload 12 files

Browse files
Files changed (12) hide show
  1. captions.json +13 -0
  2. config.json +6 -0
  3. image_converter.py +426 -0
  4. image_error_fix.py +348 -0
  5. image_filter.py +515 -0
  6. image_to_caption.py +844 -0
  7. image_to_tag.py +784 -0
  8. main.py +123 -0
  9. photo_fantasy.py +393 -0
  10. requirements.txt +0 -0
  11. rotate_flip.py +316 -0
  12. shuffle_image.py +285 -0
captions.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "prompt": "Write a concise, accurate, blunt, and detailed description. Focus on important points instead of writing too long. INCLUDE genre and style of the artwork if not photorealistic, State the ethnicity or race of any person. State what the photo was taken or drawn with. AVOID euphemisms, vague wording. Keep the caption length under 150 words!",
3
+ "max_new_tokens": 250,
4
+ "temperature": 1.0,
5
+ "top_k": 50,
6
+ "top_p": null,
7
+ "precision": 1,
8
+ "thread_count": 4,
9
+ "batch_size": 1,
10
+ "prepend_text": "",
11
+ "append_text": "",
12
+ "caption_handling": "overwrite"
13
+ }
config.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "model": "swinv2",
3
+ "general_threshold": 0.6,
4
+ "character_threshold": 0.35,
5
+ "model_dir": "D:/test/models/wd-swinv2-tagger-v3"
6
+ }
image_converter.py ADDED
@@ -0,0 +1,426 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tkinter as tk
2
+ from tkinter import filedialog, ttk, messagebox
3
+ from PIL import Image as PILImage, ImageTk
4
+ from wand.image import Image
5
+ import os
6
+ import queue
7
+ import hashlib
8
+ import threading
9
+ import subprocess
10
+
11
+ # Biến toàn cục để điều khiển việc dừng và lưu lỗi
12
+ stop_conversion = False
13
+ error_messages = []
14
+ selected_files = []
15
+ converted_hashes = set()
16
+ save_directory = ""
17
+
18
+ def open_image_converter():
19
+ global stop_conversion, error_messages, selected_files, save_dir_var, format_var, status_var, num_files_var, errors_var, thread_count_var, filename_var, filter_var, progress, q, root
20
+
21
+ # Tạo cửa sổ Tkinter
22
+ root = tk.Tk()
23
+ root.title("Image Converter")
24
+
25
+ # Khởi tạo các biến Tkinter
26
+ save_dir_var = tk.StringVar()
27
+ format_var = tk.StringVar(value='png')
28
+ status_var = tk.StringVar()
29
+ num_files_var = tk.StringVar(value="0 files selected.")
30
+ errors_var = tk.StringVar(value="Errors: 0")
31
+ thread_count_var = tk.StringVar(value="4")
32
+ filename_var = tk.StringVar()
33
+ filter_var = tk.BooleanVar(value=False)
34
+ progress = tk.IntVar(value=0)
35
+ q = queue.Queue()
36
+
37
+ # Định nghĩa các định dạng được hỗ trợ
38
+ SUPPORTED_TO_PS = ['pdf', 'jpeg', 'jpg', 'tiff', 'pnm']
39
+ SUPPORTED_TO_EPS = ['pdf', 'jpeg', 'jpg', 'tiff', 'pnm']
40
+ NEEDS_INTERMEDIATE_CONVERSION = {
41
+ 'psd': 'pdf', 'svg': 'pdf', 'png': 'pdf',
42
+ 'bmp': 'pdf', 'gif': 'pdf', 'ico': 'png'
43
+ }
44
+
45
+ # Các hàm trợ giúp
46
+ def center_window(window):
47
+ window.update_idletasks()
48
+ width = window.winfo_width()
49
+ height = window.winfo_height()
50
+ x = (window.winfo_screenwidth() // 2) - (width // 2)
51
+ y = (window.winfo_screenheight() // 2) - (height // 2)
52
+ window.geometry(f'{width}x{height}+{x}+{y}')
53
+
54
+ def select_files():
55
+ filepaths = filedialog.askopenfilenames(
56
+ title="Select Files",
57
+ filetypes=[("All Image files", "*.jpg;*.jpeg;*.png;*.gif;*.bmp;*.tiff;*.tif;*.svg;*.webp;*.pdf;*.psd;*.ico"),
58
+ ("JPEG files", "*.jpg;*.jpeg"),
59
+ ("PNG files", "*.png"),
60
+ ("GIF files", "*.gif"),
61
+ ("BMP files", "*.bmp"),
62
+ ("TIFF files", "*.tiff;*.tif"),
63
+ ("SVG files", "*.svg"),
64
+ ("WEBP files", "*.webp"),
65
+ ("PDF files", "*.pdf"),
66
+ ("PSD files", "*.psd"),
67
+ ("ICO files", "*.ico")]
68
+ )
69
+ if filepaths:
70
+ selected_files.clear()
71
+ selected_files.extend(filepaths)
72
+ num_files_var.set(f"{len(selected_files)} files selected.")
73
+ update_image_preview()
74
+
75
+ def choose_save_directory():
76
+ global save_directory
77
+ directory = filedialog.askdirectory()
78
+ if directory:
79
+ save_directory = directory
80
+ save_dir_var.set(directory)
81
+ save_dir_entry.config(state='normal')
82
+ save_dir_entry.delete(0, tk.END)
83
+ save_dir_entry.insert(0, directory)
84
+ save_dir_entry.config(state='readonly')
85
+
86
+ def hash_image(file_path):
87
+ """Tạo hàm băm SHA-256 từ nội dung ảnh."""
88
+ hash_sha256 = hashlib.sha256()
89
+ try:
90
+ file_path = os.path.normpath(file_path)
91
+ with open(file_path, "rb") as f:
92
+ for chunk in iter(lambda: f.read(4096), b""):
93
+ hash_sha256.update(chunk)
94
+ except Exception as e:
95
+ print(f"Error hashing file {file_path}: {e}")
96
+ return None
97
+ return hash_sha256.hexdigest()
98
+
99
+ def filter_duplicate_images(filepaths):
100
+ unique_images = {}
101
+ filtered_files = []
102
+ for filepath in filepaths:
103
+ image_hash = hash_image(filepath)
104
+ if image_hash and image_hash not in unique_images:
105
+ if image_hash not in converted_hashes:
106
+ unique_images[image_hash] = filepath
107
+ filtered_files.append(filepath)
108
+ return filtered_files
109
+
110
+ def can_convert_directly(input_format, output_format):
111
+ """Kiểm tra xem có thể chuyển đổi trực tiếp từ định dạng nguồn sang định dạng đích không."""
112
+ if output_format == 'ps':
113
+ return input_format in SUPPORTED_TO_PS
114
+ elif output_format == 'eps':
115
+ return input_format in SUPPORTED_TO_EPS
116
+ elif output_format == 'ico':
117
+ return input_format not in ['ps', 'eps', 'pdf']
118
+ elif input_format == 'ico':
119
+ return output_format not in ['ps', 'eps']
120
+ return True
121
+
122
+ def notify_conversion_path(input_format, output_format):
123
+ """Hiển thị thông báo nhắc nhở người dùng về bước chuyển đổi trung gian cần thiết."""
124
+ intermediate_format = NEEDS_INTERMEDIATE_CONVERSION.get(input_format, None)
125
+ if intermediate_format:
126
+ if output_format in ['ps', 'eps']:
127
+ message = (f"Cannot convert directly from {input_format.upper()} to {output_format.upper()}. "
128
+ f"Please convert to PDF first, then convert to {output_format.upper()}.")
129
+ else:
130
+ message = (f"Cannot convert directly from {input_format.upper()} to {output_format.upper()}. "
131
+ f"Please convert to {intermediate_format.upper()} first, then convert to {output_format.upper()}.")
132
+ else:
133
+ message = (f"Conversion from {input_format.upper()} to {output_format.upper()} is not supported. "
134
+ "Please use a different format for conversion.")
135
+ messagebox.showwarning("Conversion Notice", message)
136
+
137
+ def convert_image_with_wand(input_path, output_format, output_path):
138
+ """Chuyển đổi ảnh sử dụng ImageMagick thông qua Wand."""
139
+ try:
140
+ with Image(filename=input_path) as img:
141
+ img.format = output_format
142
+ img.save(filename=output_path)
143
+ except Exception as e:
144
+ raise RuntimeError(f"Error converting {input_path} to {output_format}: {e}")
145
+
146
+ def convert_image_with_ghostscript(input_path, output_format, output_path):
147
+ """Chuyển đổi ảnh sử dụng Ghostscript."""
148
+ try:
149
+ gs_command = [
150
+ "gswin64c",
151
+ "-dBATCH",
152
+ "-dNOPAUSE",
153
+ "-sDEVICE=" + ("ps2write" if output_format == "ps" else "eps2write"),
154
+ f"-sOutputFile={output_path}",
155
+ input_path
156
+ ]
157
+ subprocess.run(gs_command, check=True, creationflags=subprocess.CREATE_NO_WINDOW)
158
+ except subprocess.CalledProcessError as e:
159
+ raise RuntimeError(f"Ghostscript error: {e}")
160
+
161
+ def convert_image(input_path, save_directory, output_format, output_filename, q):
162
+ if stop_conversion:
163
+ return
164
+
165
+ filename = os.path.basename(input_path)
166
+ name, ext = os.path.splitext(filename)
167
+ ext = ext[1:].lower()
168
+ original_name = name
169
+
170
+ try:
171
+ if output_filename:
172
+ name = output_filename
173
+
174
+ output_path = os.path.join(save_directory, f"{name}.{output_format}")
175
+
176
+ counter = 1
177
+ while os.path.exists(output_path):
178
+ new_name = f"{name} ({counter})"
179
+ output_path = os.path.join(save_directory, f"{new_name}.{output_format}")
180
+ counter += 1
181
+
182
+ if ext in NEEDS_INTERMEDIATE_CONVERSION and output_format in ["ps", "eps"]:
183
+ intermediate_format = NEEDS_INTERMEDIATE_CONVERSION[ext]
184
+ intermediate_path = os.path.join(save_directory, f"{name}.{intermediate_format}")
185
+
186
+ # Chuyển đổi sang định dạng trung gian
187
+ convert_image_with_wand(input_path, intermediate_format, intermediate_path)
188
+
189
+ # Chuyển đổi từ định dạng trung gian sang định dạng cuối
190
+ if output_format in ["ps", "eps"]:
191
+ convert_image_with_ghostscript(intermediate_path, output_format, output_path)
192
+ else:
193
+ convert_image_with_wand(intermediate_path, output_format, output_path)
194
+
195
+ os.remove(intermediate_path)
196
+ else:
197
+ if output_format in ["ps", "eps"]:
198
+ convert_image_with_ghostscript(input_path, output_format, output_path)
199
+ else:
200
+ convert_image_with_wand(input_path, output_format, output_path)
201
+
202
+ q.put(input_path)
203
+ converted_hashes.add(hash_image(output_path))
204
+ except Exception as e:
205
+ error_message = f"Error converting {filename}: {str(e)}"
206
+ q.put(error_message)
207
+ error_messages.append(error_message)
208
+
209
+ def worker(save_directory, output_format, num_threads, output_filename, q, filter_duplicates):
210
+ try:
211
+ total_files = selected_files
212
+ if filter_duplicates:
213
+ total_files = filter_duplicate_images(selected_files)
214
+ progress.set(0)
215
+ for i, input_path in enumerate(total_files, 1):
216
+ if stop_conversion:
217
+ break
218
+ input_format = os.path.splitext(input_path)[1][1:].lower()
219
+ if not can_convert_directly(input_format, output_format):
220
+ notify_conversion_path(input_format, output_format)
221
+ q.put(None)
222
+ return
223
+
224
+ thread = threading.Thread(target=convert_image, args=(input_path, save_directory, output_format, output_filename, q))
225
+ thread.start()
226
+ thread.join()
227
+
228
+ q.put(None)
229
+ except Exception as e:
230
+ if not stop_conversion:
231
+ q.put(e)
232
+
233
+ def update_progress():
234
+ try:
235
+ completed = 0
236
+ while True:
237
+ item = q.get()
238
+ if item is None:
239
+ break
240
+ if isinstance(item, str):
241
+ if "Error" in item:
242
+ root.after(0, errors_var.set, f"Errors: {len(error_messages)}")
243
+ continue
244
+ completed += 1
245
+ progress.set(int((completed / len(selected_files)) * 100))
246
+ if not stop_conversion:
247
+ root.after(0, status_var.set, f"Converted {completed} files")
248
+ root.after(0, root.update_idletasks)
249
+ if not stop_conversion:
250
+ root.after(0, progress.set, 100)
251
+ show_completion_message(completed)
252
+ except Exception as e:
253
+ if not stop_conversion:
254
+ root.after(0, status_var.set, f"Error: {e}")
255
+
256
+ def show_completion_message(completed):
257
+ message = f"Conversion complete. {completed} files converted."
258
+ if error_messages:
259
+ message += f" {len(error_messages)} errors occurred."
260
+ messagebox.showinfo("Conversion Complete", message)
261
+
262
+ def convert_files():
263
+ global stop_conversion, error_messages
264
+ stop_conversion = False
265
+ error_messages.clear()
266
+ errors_var.set("Errors: 0")
267
+ save_directory = save_dir_var.get()
268
+ output_format = format_var.get()
269
+ try:
270
+ num_threads = int(thread_count_var.get() or 4)
271
+ except ValueError:
272
+ messagebox.showerror("Input Error", "Threads must be a number.")
273
+ return
274
+ output_filename = filename_var.get()
275
+ filter_duplicates = filter_var.get()
276
+ if not selected_files or not save_directory or not output_format:
277
+ status_var.set("Please select images, output format, and save location.")
278
+ return
279
+
280
+ threading.Thread(target=worker, args=(save_directory, output_format, num_threads, output_filename, q, filter_duplicates)).start()
281
+ threading.Thread(target=update_progress).start()
282
+
283
+ def stop_conversion_func():
284
+ global stop_conversion
285
+ stop_conversion = True
286
+ status_var.set("Conversion stopped.")
287
+
288
+ def return_to_menu():
289
+ stop_conversion_func()
290
+ root.destroy()
291
+ import main
292
+ main.open_main_menu()
293
+
294
+ def on_closing():
295
+ return_to_menu()
296
+
297
+ def show_errors():
298
+ global error_window
299
+ if error_window is not None:
300
+ return
301
+
302
+ error_window = tk.Toplevel(root)
303
+ error_window.title("Error Details")
304
+ error_window.geometry("500x400")
305
+
306
+ error_text = tk.Text(error_window, wrap='word')
307
+ error_text.pack(expand=True, fill='both')
308
+
309
+ if error_messages:
310
+ for error in error_messages:
311
+ error_text.insert('end', error + '\n')
312
+ else:
313
+ error_text.insert('end', "No errors recorded.")
314
+
315
+ error_text.config(state='disabled')
316
+
317
+ def on_close_error_window():
318
+ global error_window
319
+ error_window.destroy()
320
+ error_window = None
321
+
322
+ error_window.protocol("WM_DELETE_WINDOW", on_close_error_window)
323
+
324
+ def update_image_preview():
325
+ for widget in image_preview_frame.winfo_children():
326
+ widget.destroy()
327
+
328
+ for i, file_path in enumerate(selected_files):
329
+ thumbnail_size = (100, 100)
330
+ try:
331
+ image = PILImage.open(file_path)
332
+ image.thumbnail(thumbnail_size)
333
+ thumbnail = ImageTk.PhotoImage(image)
334
+
335
+ tk.Label(image_preview_frame, image=thumbnail).grid(row=i, column=0, padx=5, pady=5)
336
+ tk.Label(image_preview_frame, text=os.path.basename(file_path)).grid(row=i, column=1, padx=5, pady=5)
337
+ tk.Label(image_preview_frame, text="Caption placeholder", wraplength=300).grid(row=i, column=2, padx=5, pady=5)
338
+ except Exception as e:
339
+ tk.Label(image_preview_frame, text="Error loading image").grid(row=i, column=0, columnspan=3, padx=5, pady=5)
340
+
341
+ def validate_number(P):
342
+ if P.isdigit() or P == "":
343
+ return True
344
+ else:
345
+ messagebox.showerror("Input Error", "Please enter only numbers.")
346
+ return False
347
+
348
+ validate_command = root.register(validate_number)
349
+
350
+ # Tạo các thành phần giao diện
351
+ main_frame = tk.Frame(root)
352
+ main_frame.pack(fill=tk.BOTH, expand=True)
353
+
354
+ control_frame = tk.Frame(main_frame)
355
+ control_frame.pack(fill=tk.X, padx=10, pady=10)
356
+
357
+ back_button = tk.Button(control_frame, text="<-", font=('Helvetica', 14), command=return_to_menu)
358
+ back_button.pack(side=tk.TOP, anchor='w', padx=5, pady=5)
359
+
360
+ title_label = tk.Label(control_frame, text="Image Converter", font=('Helvetica', 16))
361
+ title_label.pack(side=tk.TOP, padx=5, pady=5)
362
+
363
+ select_button = tk.Button(control_frame, text="Select Files", command=select_files)
364
+ select_button.pack(side=tk.TOP, padx=5, pady=5)
365
+
366
+ num_files_label = tk.Label(control_frame, textvariable=num_files_var)
367
+ num_files_label.pack(side=tk.TOP, padx=5, pady=5)
368
+
369
+ save_dir_button = tk.Button(control_frame, text="Choose Save Directory", command=choose_save_directory)
370
+ save_dir_button.pack(side=tk.TOP, padx=5, pady=5)
371
+
372
+ save_dir_entry = tk.Entry(control_frame, textvariable=save_dir_var, state='readonly', justify='center')
373
+ save_dir_entry.pack(side=tk.TOP, padx=5, pady=5, fill=tk.X)
374
+
375
+ format_frame = tk.Frame(main_frame)
376
+ format_frame.pack(fill=tk.X, padx=10, pady=5)
377
+
378
+ format_label = tk.Label(format_frame, text="Output Format:")
379
+ format_label.pack(side=tk.LEFT, padx=5)
380
+
381
+ format_dropdown = ttk.Combobox(format_frame, textvariable=format_var, values=['png', 'jpg', 'gif', 'bmp', 'tiff', 'svg', 'webp', 'pdf', 'psd', 'ps', 'eps', 'ico'])
382
+ format_dropdown.pack(side=tk.LEFT, padx=5)
383
+
384
+ thread_count_label = tk.Label(format_frame, text="Threads:")
385
+ thread_count_label.pack(side=tk.LEFT, padx=5)
386
+
387
+ thread_count_entry = tk.Entry(format_frame, textvariable=thread_count_var, width=5, validate="key", validatecommand=(validate_command, '%P'), justify='center')
388
+ thread_count_entry.pack(side=tk.LEFT, padx=5)
389
+
390
+ filename_label = tk.Label(main_frame, text="Output Filename (optional):")
391
+ filename_label.pack(fill=tk.X, padx=10, pady=5)
392
+
393
+ filename_entry = tk.Entry(main_frame, textvariable=filename_var, justify='center')
394
+ filename_entry.pack(fill=tk.X, padx=10, pady=5)
395
+
396
+ filter_frame = tk.Frame(main_frame)
397
+ filter_frame.pack(fill=tk.X, padx=10, pady=5)
398
+
399
+ filter_checkbox = tk.Checkbutton(filter_frame, text="Filter duplicate images", variable=filter_var)
400
+ filter_checkbox.pack(side=tk.LEFT)
401
+
402
+ convert_button = tk.Button(main_frame, text="Convert", command=convert_files)
403
+ convert_button.pack(pady=10)
404
+
405
+ stop_button = tk.Button(main_frame, text="Stop", command=stop_conversion_func)
406
+ stop_button.pack(pady=5)
407
+
408
+ errors_button = tk.Button(main_frame, textvariable=errors_var, command=show_errors)
409
+ errors_button.pack(pady=5)
410
+
411
+ progress_bar = ttk.Progressbar(main_frame, variable=progress, maximum=100)
412
+ progress_bar.pack(pady=5, fill=tk.X)
413
+
414
+ status_label = tk.Label(main_frame, textvariable=status_var, fg="green")
415
+ status_label.pack(pady=5)
416
+
417
+ # Khung hiển thị ảnh và caption
418
+ image_preview_frame = tk.Frame(root)
419
+ image_preview_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)
420
+
421
+ center_window(root)
422
+ root.protocol("WM_DELETE_WINDOW", on_closing)
423
+ root.mainloop()
424
+
425
+ if __name__ == "__main__":
426
+ open_image_converter()
image_error_fix.py ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tkinter as tk
2
+ from tkinter import filedialog, ttk, messagebox
3
+ import os
4
+ import threading, queue
5
+ from PIL import Image, UnidentifiedImageError
6
+
7
+ # Global variables for managing state and errors
8
+ stop_processing = False
9
+ error_messages = []
10
+ selected_files = []
11
+ error_list = []
12
+
13
+ def open_image_error_fix():
14
+ global stop_processing, error_messages, selected_files, status_var, num_files_var, errors_var, progress, q, error_list, error_window, thread_count_var
15
+
16
+ # Initialize the main Tkinter window
17
+ root = tk.Tk()
18
+ root.title("Image Error Fix")
19
+
20
+ # Initialize Tkinter variables
21
+ status_var = tk.StringVar()
22
+ num_files_var = tk.StringVar()
23
+ errors_var = tk.StringVar(value="Errors: 0")
24
+ progress = tk.IntVar()
25
+ thread_count_var = tk.StringVar(value="4") # Default thread count
26
+ q = queue.Queue()
27
+
28
+ def center_window(window):
29
+ window.update_idletasks()
30
+ width = window.winfo_width() + 120
31
+ height = window.winfo_height()
32
+ x = (window.winfo_screenwidth() // 2) - (width // 2)
33
+ y = (window.winfo_screenheight() // 2) - (height // 2)
34
+ window.geometry(f'{width}x{height}+{x}+{y}')
35
+
36
+ def validate_number(P):
37
+ return P.isdigit() or P == ""
38
+
39
+ def validate_thread_count(var):
40
+ value = var.get()
41
+ if value != "":
42
+ try:
43
+ int_value = int(value)
44
+ if int_value <= 0:
45
+ raise ValueError
46
+ except ValueError:
47
+ messagebox.showerror("Invalid Input", "Please enter a valid number of threads.")
48
+ var.set("")
49
+
50
+ def select_files():
51
+ global selected_files
52
+ filetypes = [
53
+ ("All Image files", "*.jpg;*.jpeg;*.png;*.gif;*.bmp;*.tiff;*.tif;*.svg;*.webp"),
54
+ ("JPEG files", "*.jpg;*.jpeg"),
55
+ ("PNG files", "*.png"),
56
+ ("GIF files", "*.gif"),
57
+ ("BMP files", "*.bmp"),
58
+ ("TIFF files", "*.tiff;*.tif"),
59
+ ("SVG files", "*.svg"),
60
+ ("WEBP files", "*.webp")
61
+ ]
62
+ filepaths = filedialog.askopenfilenames(title="Select Image Files", filetypes=filetypes)
63
+ if filepaths:
64
+ selected_files.clear()
65
+ selected_files.extend(filepaths)
66
+ num_files_var.set(f"{len(selected_files)} files selected.")
67
+
68
+ def detect_errors(file_path):
69
+ """Detect potential errors in an image file."""
70
+ errors = []
71
+ if not os.access(file_path, os.R_OK):
72
+ errors.append("Permission Denied")
73
+ # Additional error checks
74
+ try:
75
+ with Image.open(file_path) as img:
76
+ img.verify() # Verify the image integrity
77
+ img = Image.open(file_path)
78
+ img.load() # Load the image data to ensure it's not truncated
79
+ except UnidentifiedImageError:
80
+ errors.append("Unidentified image format or corrupted file")
81
+ except IOError as e:
82
+ errors.append(f"IOError: {str(e)}")
83
+ except Exception as e:
84
+ errors.append(f"Unknown error: {str(e)}")
85
+ return errors
86
+
87
+ def fix_error(file_path, error_type):
88
+ """Fix errors in an image file, if possible."""
89
+ if error_type == "Permission Denied":
90
+ # Attempt to change permissions
91
+ try:
92
+ os.chmod(file_path, 0o644)
93
+ return "Permissions fixed. File permissions were successfully updated."
94
+ except Exception as e:
95
+ return f"Failed to fix permissions. Ensure you have the necessary permissions to modify this file. Error: {str(e)}"
96
+ elif error_type == "Unidentified image format or corrupted file":
97
+ return "The file format is unrecognized or the file is corrupted. Please try to re-download or restore from a backup."
98
+ elif error_type == "Invalid Format":
99
+ return ("Cannot automatically fix invalid formats. Ensure the file extension matches the file content.\n"
100
+ "To fix this issue, please follow these steps:\n"
101
+ "1. Identify the correct file format by using a file type checker tool (e.g., CheckFileType.com).\n"
102
+ "2. Rename the file with the correct extension:\n"
103
+ " - Right-click on the file and select 'Rename'.\n"
104
+ " - Change the file extension to the correct format (e.g., .jpg, .png).\n"
105
+ " - Press Enter to save the changes.\n"
106
+ "3. Try opening the file again. If the problem persists, the file may be corrupted or contain unsupported data.")
107
+ elif error_type == "File Not Found":
108
+ return ("The file could not be found at the specified path. Please ensure that the file has not been moved or deleted.\n"
109
+ "1. Verify the file path is correct.\n"
110
+ "2. If the file has been moved, update the path in the application or locate the file manually.\n"
111
+ "3. If the file was deleted, check your Recycle Bin or use a data recovery tool to attempt to restore it.")
112
+ elif error_type == "File Path Too Long":
113
+ return ("The file path exceeds the system limit. Windows has a maximum path length of 260 characters.\n"
114
+ "To fix this issue:\n"
115
+ "1. Move the file to a location with a shorter path, closer to the root directory (e.g., C:\\).\n"
116
+ "2. Rename folders in the path to shorter names.\n"
117
+ "3. Enable long path support in Windows if using Windows 10 (version 1607 hoặc cao hơn):\n"
118
+ " - Open the Group Policy Editor (gpedit.msc).\n"
119
+ " - Navigate to 'Local Computer Policy > Computer Configuration > Administrative Templates > System > Filesystem'.\n"
120
+ " - Enable the 'Enable Win32 long paths' option.")
121
+ elif error_type == "Transmission Errors":
122
+ return ("The file may be incomplete or corrupted due to transmission errors. This can happen if the file was downloaded or transferred incorrectly.\n"
123
+ "To fix this issue:\n"
124
+ "1. Re-download hoặc re-transfer the file.\n"
125
+ "2. Use a reliable network connection to ensure the file is not corrupted during transfer.\n"
126
+ "3. Verify the integrity of the file by comparing checksums (if available).")
127
+ elif error_type == "Unsupported Format":
128
+ return ("The file format is not supported by this application. Supported formats include JPEG, PNG, GIF, BMP, TIFF, SVG, và WEBP.\n"
129
+ "To fix this issue:\n"
130
+ "1. Convert the file to a supported format using an image converter tool.\n"
131
+ "2. Ensure that the file is not corrupted and can be opened with other image viewers or editors.")
132
+ else:
133
+ return "No action taken. This error type is not recognized or cannot be fixed automatically."
134
+
135
+ def delete_file(file_path):
136
+ """Delete the specified file."""
137
+ try:
138
+ os.remove(file_path)
139
+ # Remove from selected files
140
+ selected_files.remove(file_path)
141
+ num_files_var.set(f"{len(selected_files)} files selected.")
142
+ return f"File {file_path} deleted successfully."
143
+ except Exception as e:
144
+ return f"Failed to delete file {file_path}. Error: {str(e)}"
145
+
146
+ def process_image(file_path, q):
147
+ if stop_processing:
148
+ return
149
+
150
+ errors = detect_errors(file_path)
151
+ if errors:
152
+ q.put((file_path, errors))
153
+
154
+ def worker():
155
+ try:
156
+ progress.set(0)
157
+ total_files = len(selected_files)
158
+ thread_count = int(thread_count_var.get()) # Get the number of threads
159
+ for i, input_path in enumerate(selected_files, 1):
160
+ if stop_processing:
161
+ break
162
+
163
+ thread = threading.Thread(target=process_image, args=(input_path, q))
164
+ thread.start()
165
+ thread.join()
166
+
167
+ # Update progress bar
168
+ progress.set(int(i / total_files * 100))
169
+
170
+ q.put(None)
171
+ except Exception as e:
172
+ if not stop_processing:
173
+ q.put(e)
174
+
175
+ def update_progress():
176
+ try:
177
+ global error_list
178
+ error_list = []
179
+ while True:
180
+ item = q.get()
181
+ if item is None:
182
+ break
183
+ if isinstance(item, tuple):
184
+ error_list.append(item)
185
+
186
+ if not error_list:
187
+ messagebox.showinfo("No Errors Found", "No errors were detected in the selected images.")
188
+ else:
189
+ errors_var.set(f"Errors: {len(error_list)}")
190
+ display_errors(error_list)
191
+ except Exception as e:
192
+ if not stop_processing:
193
+ root.after(0, status_var.set, f"Error: {e}")
194
+
195
+ def update_error_display():
196
+ """Update the display of errors in the error window."""
197
+ global error_list, frame
198
+ # Clear all existing widgets in the frame
199
+ for widget in frame.winfo_children():
200
+ widget.destroy()
201
+
202
+ # Repopulate the frame with updated error list
203
+ for file_path, errors in error_list:
204
+ for error in errors:
205
+ frame_row = tk.Frame(frame)
206
+ frame_row.pack(fill="x", pady=2)
207
+
208
+ # Hiển thị đường dẫn đầy đủ và tự động xuống dòng
209
+ file_label = tk.Label(frame_row, text=f"{file_path}: {error}", anchor="w", wraplength=500, justify='left')
210
+ file_label.pack(side=tk.LEFT, fill="x", expand=True)
211
+
212
+ delete_button = tk.Button(frame_row, text="Delete", command=lambda fp=file_path: delete_file_action(fp))
213
+ delete_button.pack(side=tk.RIGHT)
214
+
215
+ fix_button = tk.Button(frame_row, text="Fix", command=lambda fp=file_path, et=error: fix_error_action(fp, et))
216
+ fix_button.pack(side=tk.RIGHT)
217
+
218
+ # Thêm dấu phân cách giữa các lỗi
219
+ separator = ttk.Separator(frame, orient='horizontal')
220
+ separator.pack(fill='x', pady=5)
221
+
222
+ frame.update_idletasks()
223
+ canvas.configure(scrollregion=canvas.bbox("all"))
224
+
225
+ def display_errors(error_list):
226
+ global error_window, canvas, frame
227
+ # Create or raise the error window
228
+ if 'error_window' in globals() and error_window.winfo_exists():
229
+ error_window.lift()
230
+ else:
231
+ error_window = tk.Toplevel(root)
232
+ error_window.title("Error Details")
233
+ error_window.geometry("600x400")
234
+
235
+ canvas = tk.Canvas(error_window)
236
+ canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
237
+
238
+ scrollbar = tk.Scrollbar(error_window, command=canvas.yview)
239
+ scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
240
+
241
+ canvas.configure(yscrollcommand=scrollbar.set)
242
+
243
+ frame = tk.Frame(canvas)
244
+ canvas.create_window((0, 0), window=frame, anchor="nw")
245
+
246
+ update_error_display()
247
+
248
+ def fix_error_action(file_path, error_type):
249
+ fix_message = fix_error(file_path, error_type)
250
+ messagebox.showinfo("Fix Error", fix_message)
251
+
252
+ def delete_file_action(file_path):
253
+ global error_list
254
+ delete_message = delete_file(file_path)
255
+ messagebox.showinfo("Delete File", delete_message)
256
+ # Remove the error from the error_list and refresh the display
257
+ error_list = [(fp, errs) for fp, errs in error_list if fp != file_path]
258
+ errors_var.set(f"Errors: {len(error_list)}")
259
+ update_error_display()
260
+
261
+ def delete_all_errors():
262
+ global error_list
263
+ for file_path, _ in error_list:
264
+ delete_file(file_path)
265
+ error_list.clear()
266
+ errors_var.set("Errors: 0")
267
+ messagebox.showinfo("Delete All Files", "All files with errors have been deleted.")
268
+ update_error_display() # Update the error display after deleting all errors
269
+
270
+ def scan_and_fix():
271
+ global stop_processing, error_messages
272
+ stop_processing = False
273
+ error_messages.clear()
274
+ errors_var.set("Errors: 0")
275
+ if not selected_files:
276
+ status_var.set("Please select images to scan.")
277
+ return
278
+
279
+ threading.Thread(target=worker).start()
280
+ threading.Thread(target=update_progress).start()
281
+
282
+ def stop_processing_func():
283
+ global stop_processing
284
+ stop_processing = True
285
+ status_var.set("Processing stopped.")
286
+
287
+ def return_to_menu():
288
+ stop_processing_func()
289
+ root.destroy()
290
+ import main
291
+ main.open_main_menu()
292
+
293
+ def on_closing():
294
+ return_to_menu()
295
+
296
+ # Create GUI elements
297
+ validate_command = root.register(validate_number)
298
+
299
+ back_button = tk.Button(root, text="<-", font=('Helvetica', 14), command=return_to_menu)
300
+ back_button.pack(anchor='nw', padx=10, pady=10)
301
+
302
+ title_label = tk.Label(root, text="Image Error Fix", font=('Helvetica', 16))
303
+ title_label.pack(pady=10)
304
+
305
+ select_files_button = tk.Button(root, text="Select Files", command=select_files)
306
+ select_files_button.pack(pady=5)
307
+
308
+ num_files_label = tk.Label(root, textvariable=num_files_var)
309
+ num_files_label.pack(pady=5)
310
+
311
+ thread_count_label = tk.Label(root, text="Number of Threads:")
312
+ thread_count_label.pack(pady=5)
313
+
314
+ thread_count_var = tk.StringVar(value="4")
315
+ thread_count_entry = tk.Entry(root, textvariable=thread_count_var, width=3, justify='center', validate="key", validatecommand=(validate_command, '%P'))
316
+ thread_count_entry.pack(pady=5)
317
+
318
+ # Separator for aesthetics
319
+ separator = ttk.Separator(root, orient='horizontal')
320
+ separator.pack(fill='x', pady=10)
321
+
322
+ scan_fix_button = tk.Button(root, text="Scan and Fix", command=scan_and_fix)
323
+ scan_fix_button.pack(pady=10)
324
+
325
+ stop_button = tk.Button(root, text="Stop", command=stop_processing_func)
326
+ stop_button.pack(pady=5)
327
+
328
+ progress_bar = ttk.Progressbar(root, variable=progress, maximum=100)
329
+ progress_bar.pack(pady=5, fill=tk.X)
330
+
331
+ delete_all_label = tk.Label(root, text="Click 'Delete All' to remove all files with errors.")
332
+ delete_all_label.pack(pady=5)
333
+
334
+ delete_all_button = tk.Button(root, text="Delete All", command=delete_all_errors)
335
+ delete_all_button.pack(pady=5)
336
+
337
+ errors_label = tk.Label(root, textvariable=errors_var, fg="red")
338
+ errors_label.pack(pady=5)
339
+
340
+ status_label = tk.Label(root, textvariable=status_var, fg="green")
341
+ status_label.pack(pady=5)
342
+
343
+ center_window(root)
344
+ root.protocol("WM_DELETE_WINDOW", on_closing)
345
+ root.mainloop()
346
+
347
+ if __name__ == "__main__":
348
+ open_image_error_fix()
image_filter.py ADDED
@@ -0,0 +1,515 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tkinter as tk
2
+ from tkinter import filedialog, messagebox, ttk
3
+ import os
4
+ import threading
5
+ import queue
6
+ import hashlib
7
+ import shutil
8
+ from PIL import Image
9
+
10
+ # Global variables for controlling filtering and error handling
11
+ stop_event = threading.Event()
12
+ error_messages = []
13
+ error_window = None
14
+ filtered_hashes = set()
15
+ selected_files = []
16
+ worker_thread = None
17
+
18
+ def open_image_filter():
19
+ global error_messages, error_window, filtered_hashes, selected_files
20
+ global save_dir_var, status_var, num_files_var, errors_var, thread_count_var, progress
21
+ global q, format_filter_var, filter_duplicate_var, min_size_var, max_size_var
22
+ global min_total_resolution_var, max_total_resolution_var, format_mode_var, format_filter_label
23
+ global delete_originals_var, worker_thread, root, stop_button, saved_files
24
+
25
+ # Create the Tkinter window
26
+ root = tk.Tk()
27
+ root.title("Image Filter")
28
+
29
+ # Initialize Tkinter variables
30
+ save_dir_var = tk.StringVar()
31
+ status_var = tk.StringVar()
32
+ num_files_var = tk.StringVar()
33
+ errors_var = tk.StringVar(value="Errors: 0")
34
+ thread_count_var = tk.StringVar(value="4")
35
+ progress = tk.IntVar()
36
+ q = queue.Queue()
37
+ format_filter_var = tk.StringVar()
38
+ filter_duplicate_var = tk.BooleanVar()
39
+ min_size_var = tk.IntVar()
40
+ max_size_var = tk.IntVar()
41
+ min_total_resolution_var = tk.IntVar()
42
+ max_total_resolution_var = tk.IntVar()
43
+ format_mode_var = tk.StringVar(value="exclude") # 'include' or 'exclude'
44
+
45
+ # Initialize variable for deleting original images
46
+ delete_originals_var = tk.BooleanVar()
47
+
48
+ def center_window(window):
49
+ window.update_idletasks()
50
+ width = window.winfo_width() + 120 # Add 120 pixels to width
51
+ height = window.winfo_height()
52
+ x = (window.winfo_screenwidth() // 2) - (width // 2)
53
+ y = (window.winfo_screenheight() // 2) - (height // 2)
54
+ window.geometry(f'{width}x{height}+{x}+{y}')
55
+
56
+ def select_directory():
57
+ filepaths = filedialog.askopenfilenames(
58
+ title="Select Images",
59
+ filetypes=[("All Image files", "*.jpg;*.jpeg;*.png;*.gif;*.bmp;*.tiff")]
60
+ )
61
+ if filepaths:
62
+ selected_files.clear()
63
+ selected_files.extend(filepaths)
64
+ update_selected_files_label()
65
+
66
+ def choose_directory():
67
+ directory = filedialog.askdirectory()
68
+ if directory:
69
+ save_dir_var.set(directory)
70
+ save_dir_entry.config(state='normal')
71
+ save_dir_entry.delete(0, tk.END)
72
+ save_dir_entry.insert(0, directory)
73
+ save_dir_entry.config(state='readonly')
74
+
75
+ def hash_image(file_path):
76
+ """Create SHA-256 hash of image content."""
77
+ hash_sha256 = hashlib.sha256()
78
+ try:
79
+ with open(file_path, "rb") as f:
80
+ for chunk in iter(lambda: f.read(4096), b""):
81
+ hash_sha256.update(chunk)
82
+ except Exception as e:
83
+ error_messages.append(f"Error hashing file {file_path}: {e}")
84
+ update_error_count()
85
+ return None
86
+ return hash_sha256.hexdigest()
87
+
88
+ def filter_duplicate_images(filepaths):
89
+ unique_images = {}
90
+ filtered_files = []
91
+ for filepath in filepaths:
92
+ image_hash = hash_image(filepath)
93
+ if image_hash and image_hash not in filtered_hashes:
94
+ unique_images[image_hash] = filepath
95
+ filtered_hashes.add(image_hash)
96
+ else:
97
+ filtered_files.append(filepath) # Duplicate images will be added to the list
98
+ return filtered_files
99
+
100
+ def parse_formats(format_string):
101
+ return [fmt.strip().lower() for fmt in format_string.split(',') if fmt.strip()]
102
+
103
+ def filter_image_formats(filepaths, include_formats):
104
+ filtered_files = []
105
+ formats = parse_formats(format_filter_var.get())
106
+ if not formats:
107
+ return filepaths # No filtering if the format list is empty
108
+
109
+ for filepath in filepaths:
110
+ ext = os.path.splitext(filepath)[1][1:].lower() # Get the file extension
111
+ if (ext in formats) == include_formats:
112
+ filtered_files.append(filepath)
113
+ return filtered_files
114
+
115
+ def filter_image_size(filepaths, min_size, max_size):
116
+ filtered_files = []
117
+ for filepath in filepaths:
118
+ size = os.path.getsize(filepath)
119
+ if (min_size <= 0 or size >= min_size) and (max_size <= 0 or size <= max_size):
120
+ filtered_files.append(filepath)
121
+ return filtered_files
122
+
123
+ def filter_image_resolution(filepaths, min_total_resolution, max_total_resolution):
124
+ filtered_files = []
125
+ for filepath in filepaths:
126
+ try:
127
+ image = Image.open(filepath)
128
+ width, height = image.size
129
+ total_resolution = width + height
130
+ if (min_total_resolution <= 0 or total_resolution >= min_total_resolution) and \
131
+ (max_total_resolution <= 0 or total_resolution <= max_total_resolution):
132
+ filtered_files.append(filepath)
133
+ except Exception as e:
134
+ error_messages.append(f"Error reading image {filepath}: {e}")
135
+ update_error_count()
136
+ continue
137
+ return filtered_files
138
+
139
+ def save_file_with_unique_name(filepath, save_directory, saved_files):
140
+ """Save file with a unique name to avoid overwriting."""
141
+ if filepath in saved_files:
142
+ return # File already saved, do not save again
143
+
144
+ base_name, ext = os.path.splitext(os.path.basename(filepath))
145
+ save_path = os.path.join(save_directory, f"{base_name}{ext}")
146
+ counter = 1
147
+ while os.path.exists(save_path):
148
+ save_path = os.path.join(save_directory, f"{base_name} ({counter}){ext}")
149
+ counter += 1
150
+ try:
151
+ shutil.copy(filepath, save_path)
152
+ saved_files.add(filepath) # Mark this file as saved
153
+ except Exception as e:
154
+ error_messages.append(f"Error saving file {filepath}: {e}")
155
+ update_error_count()
156
+
157
+ def delete_original_images():
158
+ """Delete the original images if delete_originals_var is set."""
159
+ if delete_originals_var.get():
160
+ # Iterate through a copy of selected_files to avoid modifying the list during iteration
161
+ for filepath in selected_files[:]:
162
+ try:
163
+ os.remove(filepath)
164
+ selected_files.remove(filepath) # Remove from selected_files if deleted
165
+ except FileNotFoundError:
166
+ error_messages.append(f"File not found for deletion: {filepath}")
167
+ update_error_count()
168
+ except Exception as e:
169
+ error_messages.append(f"Error deleting file {filepath}: {e}")
170
+ update_error_count()
171
+ update_selected_files_label()
172
+
173
+ def update_selected_files_label():
174
+ """Update the label showing the number of selected files."""
175
+ num_files_var.set(f"{len(selected_files)} files selected.")
176
+
177
+ def update_error_count():
178
+ """Update the error count displayed in the Errors button."""
179
+ errors_var.set(f"Errors: {len(error_messages)}")
180
+
181
+ def check_all_files_filtered(filtered_files, filter_type):
182
+ """Check if all files have been filtered out and display a specific error message."""
183
+ if not filtered_files:
184
+ error_message = f"All images would be filtered out by the selected {filter_type} filter. Please adjust the filter settings."
185
+ messagebox.showerror("Filtering Error", error_message)
186
+ return True
187
+ return False
188
+
189
+ def filter_images_preview(filepaths):
190
+ """
191
+ Preview the number of images left after applying the filters.
192
+ Return the count of images left after filtering.
193
+ """
194
+ filtered_files = filepaths[:]
195
+
196
+ # Preview filtering by image format
197
+ include_formats = format_mode_var.get() == "include"
198
+ filtered_files = filter_image_formats(filtered_files, include_formats)
199
+
200
+ # Preview filtering by image size
201
+ filtered_files = filter_image_size(filtered_files, min_size_var.get(), max_size_var.get())
202
+
203
+ # Preview filtering by total resolution
204
+ filtered_files = filter_image_resolution(filtered_files, min_total_resolution_var.get(), max_total_resolution_var.get())
205
+
206
+ # Preview filtering duplicates if selected
207
+ if filter_duplicate_var.get():
208
+ filtered_files = filter_duplicate_images(filtered_files)
209
+
210
+ return len(filtered_files)
211
+
212
+ def filter_images(save_directory):
213
+ global saved_files
214
+ saved_files = set() # Initialize saved_files set
215
+ num_initial_files = 0 # Initialize before try-except block
216
+ try:
217
+ num_initial_files = len(selected_files)
218
+ filtered_files = selected_files[:]
219
+
220
+ # Filter by image format
221
+ include_formats = format_mode_var.get() == "include"
222
+ filtered_files = filter_image_formats(filtered_files, include_formats)
223
+ if check_all_files_filtered(filtered_files, "format"):
224
+ return [], num_initial_files, 0, 0
225
+
226
+ # Filter by image size
227
+ filtered_files = filter_image_size(filtered_files, min_size_var.get(), max_size_var.get())
228
+ if check_all_files_filtered(filtered_files, "size"):
229
+ return [], num_initial_files, 0, 0
230
+
231
+ # Filter by total resolution
232
+ filtered_files = filter_image_resolution(filtered_files, min_total_resolution_var.get(), max_total_resolution_var.get())
233
+ if check_all_files_filtered(filtered_files, "resolution"):
234
+ return [], num_initial_files, 0, 0
235
+
236
+ # Filter duplicates if selected
237
+ if filter_duplicate_var.get():
238
+ filtered_files = filter_duplicate_images(filtered_files)
239
+ if check_all_files_filtered(filtered_files, "duplicate"):
240
+ return [], num_initial_files, 0, 0
241
+
242
+ # Calculate the number of filtered out images
243
+ num_filtered_files = len(filtered_files)
244
+ num_filtered_out_files = num_initial_files - num_filtered_files
245
+
246
+ if not os.path.exists(save_directory):
247
+ os.makedirs(save_directory)
248
+ for file in filtered_files:
249
+ save_file_with_unique_name(file, save_directory, saved_files)
250
+
251
+ return filtered_files, num_initial_files, num_filtered_files, num_filtered_out_files
252
+
253
+ except Exception as e:
254
+ error_messages.append(str(e))
255
+ update_error_count()
256
+ return [], num_initial_files, 0, 0
257
+
258
+ def worker(save_directory, num_threads, q):
259
+ try:
260
+ filtered_files, num_initial_files, num_filtered_files, num_filtered_out_files = filter_images(save_directory)
261
+ if not filtered_files: # Check again if any files left after filtering
262
+ return # Stop if no files left
263
+ for i, file in enumerate(filtered_files):
264
+ if stop_event.is_set():
265
+ break
266
+ save_file_with_unique_name(file, save_directory, saved_files)
267
+ progress.set(int((i + 1) / num_initial_files * 100))
268
+ q.put((filtered_files, num_initial_files, num_filtered_files, num_filtered_out_files))
269
+ q.put(None)
270
+ except Exception as e:
271
+ if not stop_event.is_set():
272
+ error_messages.append(str(e))
273
+ update_error_count()
274
+ q.put(str(e))
275
+ finally:
276
+ stop_event.clear() # Clear the stop event for the next run
277
+
278
+ def update_progress():
279
+ try:
280
+ completed = 0
281
+ while True:
282
+ item = q.get()
283
+ if item is None:
284
+ break
285
+ if isinstance(item, tuple):
286
+ filtered_files, num_initial_files, num_filtered_files, num_filtered_out_files = item
287
+ completed += 1
288
+ progress.set(int((completed / num_initial_files) * 100))
289
+ root.after(0, root.update_idletasks)
290
+ elif isinstance(item, str):
291
+ if "Error" in item:
292
+ error_messages.append(item)
293
+ root.after(0, update_error_count)
294
+ continue
295
+ if not stop_event.is_set():
296
+ root.after(0, progress.set(100))
297
+ show_completion_message(num_initial_files, num_filtered_files, num_filtered_out_files)
298
+ delete_original_images() # Delete original images after completion
299
+ # Re-enable all buttons after completion
300
+ root.after(0, lambda: filter_button.config(state='normal'))
301
+ root.after(0, lambda: select_directory_button.config(state='normal'))
302
+ root.after(0, lambda: choose_dir_button.config(state='normal'))
303
+ root.after(0, lambda: delete_originals_checkbox.config(state='normal'))
304
+ except Exception as e:
305
+ if not stop_event.is_set():
306
+ error_messages.append(str(e))
307
+ root.after(0, update_error_count)
308
+ root.after(0, status_var.set, f"Error: {e}")
309
+
310
+ def show_completion_message(num_initial_files, num_filtered_files, num_filtered_out_files):
311
+ message = (
312
+ f"Filtering complete.\n"
313
+ f"Total files selected: {num_initial_files}\n"
314
+ f"Files processed and saved: {num_filtered_files}\n"
315
+ f"Files filtered out: {num_filtered_out_files}\n"
316
+ f"{len(error_messages)} errors occurred."
317
+ )
318
+ messagebox.showinfo("Filtering Complete", message)
319
+
320
+ def filter_files():
321
+ global error_messages, error_window, worker_thread
322
+ stop_event.clear() # Clear the stop event before starting a new task
323
+ error_messages.clear()
324
+ update_error_count()
325
+ save_directory = save_dir_var.get()
326
+ try:
327
+ num_threads = int(thread_count_var.get() or 4)
328
+ if num_threads <= 0:
329
+ raise ValueError("Number of threads must be greater than 0.")
330
+ except ValueError as e:
331
+ messagebox.showerror("Input Error", f"Invalid number of threads: {e}")
332
+ return
333
+
334
+ if not selected_files or not save_directory:
335
+ status_var.set("Please select images and save location.")
336
+ return
337
+
338
+ # Preview filtered results
339
+ remaining_images = filter_images_preview(selected_files)
340
+ if remaining_images == 0:
341
+ messagebox.showerror("Filtering Error", "No images will remain after applying the filters. Please adjust the filter settings.")
342
+ return
343
+
344
+ # Disable all buttons except Stop button
345
+ filter_button.config(state='disabled')
346
+ select_directory_button.config(state='disabled')
347
+ choose_dir_button.config(state='disabled')
348
+ delete_originals_checkbox.config(state='disabled')
349
+
350
+ worker_thread = threading.Thread(target=worker, args=(save_directory, num_threads, q))
351
+ worker_thread.start()
352
+ threading.Thread(target=update_progress).start()
353
+
354
+ def stop_filtering_func():
355
+ stop_event.set() # Signal the worker thread to stop
356
+ status_var.set("Filtering stopped.")
357
+ # Do not disable the Stop button to keep it always enabled
358
+ # Re-enable all buttons
359
+ filter_button.config(state='normal')
360
+ select_directory_button.config(state='normal')
361
+ choose_dir_button.config(state='normal')
362
+ delete_originals_checkbox.config(state='normal')
363
+ if worker_thread is not None:
364
+ worker_thread.join() # Wait for worker thread to finish
365
+
366
+ def return_to_menu():
367
+ stop_filtering_func()
368
+ root.destroy()
369
+ # Import main menu and open it
370
+ from main import open_main_menu
371
+ open_main_menu()
372
+
373
+ def on_closing():
374
+ stop_filtering_func()
375
+ return_to_menu()
376
+
377
+ def show_errors():
378
+ global error_window
379
+ if error_window is not None:
380
+ return
381
+
382
+ error_window = tk.Toplevel(root)
383
+ error_window.title("Error Details")
384
+ error_window.geometry("500x400")
385
+
386
+ error_text = tk.Text(error_window, wrap='word')
387
+ error_text.pack(expand=True, fill='both')
388
+
389
+ if error_messages:
390
+ for error in error_messages:
391
+ error_text.insert('end', error + '\n')
392
+ else:
393
+ error_text.insert('end', "No errors recorded.")
394
+
395
+ error_text.config(state='disabled')
396
+
397
+ def on_close_error_window():
398
+ global error_window
399
+ error_window.destroy()
400
+ error_window = None
401
+
402
+ error_window.protocol("WM_DELETE_WINDOW", on_close_error_window)
403
+
404
+ def toggle_format_mode():
405
+ if format_mode_var.get() == "include":
406
+ format_filter_label.config(text="Include Image Formats (comma-separated, e.g., png,jpg):")
407
+ else:
408
+ format_filter_label.config(text="Exclude Image Formats (comma-separated, e.g., png,jpg):")
409
+
410
+ def validate_number(P):
411
+ if P.isdigit() or P == "":
412
+ return True
413
+ else:
414
+ messagebox.showerror("Input Error", "Please enter only numbers.")
415
+ return False
416
+
417
+ validate_command = root.register(validate_number)
418
+
419
+ # Create UI elements
420
+ back_button = tk.Button(root, text="<-", font=('Helvetica', 14), command=return_to_menu)
421
+ back_button.pack(anchor='nw', padx=10, pady=10)
422
+
423
+ title_label = tk.Label(root, text="Image Filter", font=('Helvetica', 16))
424
+ title_label.pack(pady=10)
425
+
426
+ select_directory_button = tk.Button(root, text="Select Images", command=select_directory)
427
+ select_directory_button.pack(pady=5)
428
+
429
+ num_files_label = tk.Label(root, textvariable=num_files_var)
430
+ num_files_label.pack(pady=5)
431
+
432
+ choose_dir_button = tk.Button(root, text="Choose Save Directory", command=choose_directory)
433
+ choose_dir_button.pack(pady=5)
434
+
435
+ save_dir_entry = tk.Entry(root, textvariable=save_dir_var, state='readonly', justify='center')
436
+ save_dir_entry.pack(pady=5, fill=tk.X)
437
+
438
+ # Checkbox to delete original images
439
+ delete_originals_checkbox = tk.Checkbutton(root, text="Delete Original Images After Filtering", variable=delete_originals_var)
440
+ delete_originals_checkbox.pack(pady=5)
441
+
442
+ # Toggle image format filter mode
443
+ format_mode_frame = tk.Frame(root)
444
+ format_mode_frame.pack(pady=5)
445
+ format_mode_label = tk.Label(format_mode_frame, text="Toggle Format Mode (Include/Exclude):")
446
+ format_mode_label.pack(side="left")
447
+
448
+ # Radio buttons
449
+ include_radio = tk.Radiobutton(format_mode_frame, text="Include Formats", variable=format_mode_var, value="include", command=toggle_format_mode)
450
+ include_radio.pack(side="left", padx=5)
451
+ exclude_radio = tk.Radiobutton(format_mode_frame, text="Exclude Formats", variable=format_mode_var, value="exclude", command=toggle_format_mode)
452
+ exclude_radio.pack(side="left")
453
+
454
+ # Description for image format filter mode
455
+ format_filter_label = tk.Label(root, text="Exclude Image Formats (comma-separated, e.g., png,jpg):")
456
+ format_filter_label.pack(pady=5)
457
+
458
+ format_filter_entry = tk.Entry(root, textvariable=format_filter_var, justify='center')
459
+ format_filter_entry.pack(pady=5, fill=tk.X)
460
+
461
+ min_size_label = tk.Label(root, text="Min Size (bytes):")
462
+ min_size_label.pack(pady=5)
463
+
464
+ min_size_entry = tk.Entry(root, textvariable=min_size_var, validate="key", validatecommand=(validate_command, '%P'), justify='center', width=8)
465
+ min_size_entry.pack(pady=5)
466
+
467
+ max_size_label = tk.Label(root, text="Max Size (bytes):")
468
+ max_size_label.pack(pady=5)
469
+
470
+ max_size_entry = tk.Entry(root, textvariable=max_size_var, validate="key", validatecommand=(validate_command, '%P'), justify='center', width=8)
471
+ max_size_entry.pack(pady=5)
472
+
473
+ min_resolution_label = tk.Label(root, text="Min Total Resolution (sum of width and height):")
474
+ min_resolution_label.pack(pady=5)
475
+
476
+ min_resolution_entry = tk.Entry(root, textvariable=min_total_resolution_var, validate="key", validatecommand=(validate_command, '%P'), justify='center', width=8)
477
+ min_resolution_entry.pack(pady=5)
478
+
479
+ max_resolution_label = tk.Label(root, text="Max Total Resolution (sum of width and height):")
480
+ max_resolution_label.pack(pady=5)
481
+
482
+ max_resolution_entry = tk.Entry(root, textvariable=max_total_resolution_var, validate="key", validatecommand=(validate_command, '%P'), justify='center', width=8)
483
+ max_resolution_entry.pack(pady=5)
484
+
485
+ # Add label and entry for thread count
486
+ thread_count_label = tk.Label(root, text="Number of Threads:")
487
+ thread_count_label.pack(pady=5)
488
+
489
+ thread_count_entry = tk.Entry(root, textvariable=thread_count_var, validate="key", validatecommand=(validate_command, '%P'), justify='center', width=4)
490
+ thread_count_entry.pack(pady=5)
491
+
492
+ filter_duplicate_checkbox = tk.Checkbutton(root, text="Filter Duplicate Images", variable=filter_duplicate_var)
493
+ filter_duplicate_checkbox.pack(pady=5)
494
+
495
+ filter_button = tk.Button(root, text="Filter", command=filter_files)
496
+ filter_button.pack(pady=10)
497
+
498
+ stop_button = tk.Button(root, text="Stop", command=stop_filtering_func) # Ensure stop button is a global variable
499
+ stop_button.pack(pady=5)
500
+
501
+ errors_button = tk.Button(root, textvariable=errors_var, command=show_errors)
502
+ errors_button.pack(pady=5)
503
+
504
+ progress_bar = ttk.Progressbar(root, variable=progress, maximum=100)
505
+ progress_bar.pack(pady=5, fill=tk.X)
506
+
507
+ status_label = tk.Label(root, textvariable=status_var, fg="green")
508
+ status_label.pack(pady=5)
509
+
510
+ center_window(root)
511
+ root.protocol("WM_DELETE_WINDOW", on_closing)
512
+ root.mainloop()
513
+
514
+ if __name__ == "__main__":
515
+ open_image_filter()
image_to_caption.py ADDED
@@ -0,0 +1,844 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tkinter as tk
2
+ from tkinter import filedialog, messagebox, ttk
3
+ from PIL import Image as PILImage, ImageTk
4
+ import os
5
+ import queue
6
+ import threading
7
+ import torch
8
+ from transformers import AutoModelForCausalLM, LlamaTokenizer
9
+ import json
10
+ import traceback
11
+ import math
12
+
13
+ torch.set_grad_enabled(False)
14
+
15
+ stop_processing = False
16
+ error_messages = []
17
+ selected_files = []
18
+ save_directory = ""
19
+ caption_window = None
20
+ caption_frame = None
21
+ thumbnails = []
22
+ caption_text_widgets = []
23
+ error_window = None
24
+ status_var = None
25
+ num_files_var = None
26
+ errors_var = None
27
+ progress = None
28
+ prompt_var = None
29
+ max_new_tokens_var = None
30
+ do_sample_var = None
31
+ temperature_var = None
32
+ top_k_var = None
33
+ top_p_var = None
34
+ thread_count_var = None
35
+ precision_var = None
36
+ batch_size_var = None
37
+ prepend_text_var = None
38
+ append_text_var = None
39
+ caption_handling_var = None # Variable to handle radio buttons for caption handling
40
+ start_button = None
41
+ stop_button = None
42
+ model = None
43
+ prompt_entry = None
44
+ select_files_button = None
45
+ show_captions_button = None
46
+ thread_count_entry = None
47
+ precision_entry = None
48
+ batch_size_entry = None
49
+ prepend_text_entry = None
50
+ append_text_entry = None
51
+ root = None
52
+ q = queue.Queue()
53
+
54
+ current_page = 0
55
+ images_per_page = 20
56
+ total_pages = 1
57
+ content_canvas = None
58
+ search_var = None
59
+ original_selected_files = []
60
+ action_var = None
61
+ action_entry = None
62
+
63
+ def load_model():
64
+ global model, tokenizer
65
+ if model is None:
66
+ tokenizer = LlamaTokenizer.from_pretrained('lmsys/vicuna-7b-v1.5')
67
+ dtype = torch.float16 if precision_var.get() <= 1 else torch.float32
68
+ model = AutoModelForCausalLM.from_pretrained(
69
+ 'THUDM/cogvlm-chat-hf',
70
+ torch_dtype=dtype,
71
+ low_cpu_mem_usage=True,
72
+ trust_remote_code=True,
73
+ ).to('cuda').eval()
74
+
75
+ def update_and_save_config():
76
+ top_p_value = top_p_var.get() if do_sample_var.get() else None
77
+ config_entry = {
78
+ 'prompt': prompt_var.get(),
79
+ 'max_new_tokens': max_new_tokens_var.get(),
80
+ 'temperature': temperature_var.get(),
81
+ 'top_k': top_k_var.get(),
82
+ 'top_p': float(top_p_value) if top_p_value is not None else None,
83
+ 'precision': precision_var.get(),
84
+ 'thread_count': thread_count_var.get(),
85
+ 'batch_size': batch_size_var.get(),
86
+ 'prepend_text': prepend_text_var.get(),
87
+ 'append_text': append_text_var.get(),
88
+ 'caption_handling': caption_handling_var.get() # Save the selected caption handling option
89
+ }
90
+
91
+ try:
92
+ with open('captions.json', 'w') as f:
93
+ json.dump(config_entry, f, indent=2)
94
+ except Exception as e:
95
+ print(f"Error saving config to captions.json: {e}")
96
+
97
+ def load_config_from_json():
98
+ global prompt_entry
99
+ try:
100
+ if os.path.exists('captions.json'):
101
+ with open('captions.json', 'r') as f:
102
+ config_entry = json.load(f)
103
+ prompt_var.set(config_entry.get('prompt', ''))
104
+ max_new_tokens_var.set(config_entry.get('max_new_tokens', 200))
105
+ temperature_var.set(config_entry.get('temperature', 1.0))
106
+ top_k_var.set(config_entry.get('top_k', 50))
107
+ top_p_value = config_entry.get('top_p', 0.95)
108
+ top_p_var.set(top_p_value if top_p_value is not None else 0.95)
109
+ precision_var.set(config_entry.get('precision', 1))
110
+ thread_count_var.set(config_entry.get('thread_count', 4))
111
+ batch_size_var.set(config_entry.get('batch_size', 1))
112
+ prepend_text_var.set(config_entry.get('prepend_text', ''))
113
+ append_text_var.set(config_entry.get('append_text', ''))
114
+ caption_handling_var.set(config_entry.get('caption_handling', 'skip')) # Load the saved caption handling option
115
+
116
+ prompt_entry.delete("1.0", tk.END)
117
+ prompt_entry.insert(tk.END, config_entry.get('prompt', ''))
118
+ except Exception as e:
119
+ print(f"Error loading config from captions.json: {e}")
120
+
121
+ def on_config_change(*args):
122
+ root.after(100, update_config)
123
+
124
+ def update_config():
125
+ try:
126
+ precision_value = precision_var.get()
127
+ if precision_value == "":
128
+ return # Không làm gì nếu giá trị là chuỗi rỗng
129
+
130
+ update_and_save_config()
131
+ except Exception as e:
132
+ print(f"Lỗi khi xử lý giá trị: {e}")
133
+
134
+ def on_prompt_change(event=None):
135
+ prompt_var.set(prompt_entry.get("1.0", tk.END).strip())
136
+ update_and_save_config()
137
+
138
+ def show_errors():
139
+ global error_window
140
+ if error_window is not None:
141
+ return
142
+
143
+ error_window = tk.Toplevel(root)
144
+ error_window.title("Error Details")
145
+ error_window.geometry("500x400")
146
+
147
+ error_text = tk.Text(error_window, wrap='word')
148
+ error_text.pack(expand=True, fill='both')
149
+
150
+ if error_messages:
151
+ for error in error_messages:
152
+ error_text.insert('end', error + '\n')
153
+ else:
154
+ error_text.insert('end', "No errors recorded.")
155
+
156
+ error_text.config(state='disabled')
157
+
158
+ def on_close_error_window():
159
+ global error_window
160
+ error_window.destroy()
161
+ error_window = None
162
+
163
+ error_window.protocol("WM_DELETE_WINDOW", on_close_error_window)
164
+
165
+ def validate_numeric_input(value):
166
+ if value == "" or value == "-":
167
+ return True
168
+ try:
169
+ float(value)
170
+ return True
171
+ except ValueError:
172
+ return False
173
+
174
+ def center_window(window):
175
+ window.update_idletasks()
176
+ width = window.winfo_width()
177
+ height = window.winfo_height()
178
+ x = (window.winfo_screenwidth() // 2) - (width // 2)
179
+ y = (window.winfo_screenheight() // 2) - (height // 2)
180
+ window.geometry(f'{width}x{height}+{x}+{y}')
181
+
182
+ def toggle_sampling_options():
183
+ if do_sample_var.get():
184
+ temperature_label.pack(pady=5, after=do_sample_check)
185
+ temperature_entry.pack(pady=5, after=temperature_label)
186
+ top_k_label.pack(pady=5, after=temperature_entry)
187
+ top_k_entry.pack(pady=5, after=top_k_label)
188
+ top_p_label.pack(pady=5, after=top_k_entry)
189
+ top_p_entry.pack(pady=5, after=top_p_label)
190
+ root.geometry(f"{root.winfo_width()}x{root.winfo_height() + 150}")
191
+ else:
192
+ temperature_label.pack_forget()
193
+ temperature_entry.pack_forget()
194
+ top_k_label.pack_forget()
195
+ top_k_entry.pack_forget()
196
+ top_p_label.pack_forget()
197
+ top_p_entry.pack_forget()
198
+ root.geometry(f"{root.winfo_width()}x{root.winfo_height() - 150}")
199
+ center_window(root)
200
+
201
+ def open_image_to_caption():
202
+ global stop_processing, error_messages, selected_files, save_directory, status_var, num_files_var, errors_var, progress
203
+ global prompt_var, max_new_tokens_var, do_sample_var, temperature_var, top_k_var, top_p_var, thread_count_var, precision_var, batch_size_var
204
+ global prepend_text_var, append_text_var, search_var, action_var, caption_handling_var # Updated caption handling variable
205
+ global start_button, stop_button
206
+ global temperature_label, temperature_entry, top_k_label, top_k_entry, top_p_label, top_p_entry
207
+ global do_sample_check, prompt_entry, select_files_button, show_captions_button, thread_count_entry, precision_entry, batch_size_entry
208
+ global prepend_text_entry, append_text_entry
209
+ global root
210
+ global q
211
+
212
+ root = tk.Tk()
213
+ root.title("Image to Caption")
214
+ root.geometry("1050x950")
215
+
216
+ # Khởi tạo các biến Tkinter sau khi root đã được tạo
217
+ status_var = tk.StringVar()
218
+ num_files_var = tk.StringVar()
219
+ errors_var = tk.StringVar(value="Errors: 0")
220
+ progress = tk.IntVar()
221
+ prompt_var = tk.StringVar(value="Describe this image")
222
+ max_new_tokens_var = tk.IntVar(value=200)
223
+ do_sample_var = tk.BooleanVar(value=False)
224
+ temperature_var = tk.DoubleVar(value=1.0)
225
+ top_k_var = tk.IntVar(value=50)
226
+ top_p_var = tk.DoubleVar(value=0.95)
227
+ thread_count_var = tk.IntVar(value=4)
228
+ precision_var = tk.IntVar(value=1)
229
+ batch_size_var = tk.IntVar(value=1)
230
+ prepend_text_var = tk.StringVar()
231
+ append_text_var = tk.StringVar()
232
+ caption_handling_var = tk.StringVar(value='skip') # Default value is 'skip'
233
+ search_var = tk.StringVar() # Biến search_var khởi tạo ở đây
234
+ action_var = tk.StringVar() # Biến action_var khởi tạo ở đây
235
+
236
+ q = queue.Queue()
237
+
238
+ validate_cmd = root.register(validate_numeric_input)
239
+
240
+ back_button = tk.Button(root, text="<-", font=('Helvetica', 14), command=return_to_menu)
241
+ back_button.pack(anchor='nw', padx=10, pady=10)
242
+
243
+ title_label = tk.Label(root, text="Image Caption Generator", font=('Helvetica', 16))
244
+ title_label.pack(pady=10)
245
+
246
+ warning_label = tk.Label(root, text="NOTE: To run CogVLM with the minimum configuration, you need at least 40GB RAM to load the model in FP16 with batch size 1 and a GPU with at least 24GB of VRAM. It is recommended to install ImageDucHaiten on an NVMe SSD to optimize speed.",
247
+ font=('Helvetica', 10), fg="red", wraplength=750, justify="left")
248
+ warning_label.pack(pady=10)
249
+
250
+ select_files_button = tk.Button(root, text="Select Files", command=select_files)
251
+ select_files_button.pack(pady=10)
252
+
253
+ show_captions_button = tk.Button(root, text="Show Captions", command=open_caption_window)
254
+ show_captions_button.pack(pady=10)
255
+
256
+ num_files_label = tk.Label(root, textvariable=num_files_var)
257
+ num_files_label.pack(pady=5)
258
+
259
+ prompt_label = tk.Label(root, text="Prompt (text to describe the image):")
260
+ prompt_label.pack(pady=5)
261
+ prompt_entry = tk.Text(root, height=3, wrap='word', width=60)
262
+ prompt_entry.pack(pady=5, padx=10, fill='both', expand=True)
263
+ prompt_entry.bind('<KeyRelease>', on_prompt_change)
264
+
265
+ prepend_text_label = tk.Label(root, text="Prepend Text:")
266
+ prepend_text_label.pack(pady=5)
267
+ prepend_text_entry = tk.Entry(root, textvariable=prepend_text_var, justify='center', width=60)
268
+ prepend_text_entry.pack(pady=5)
269
+
270
+ append_text_label = tk.Label(root, text="Append Text:")
271
+ append_text_label.pack(pady=5)
272
+ append_text_entry = tk.Entry(root, textvariable=append_text_var, justify='center', width=60)
273
+ append_text_entry.pack(pady=5)
274
+
275
+ # Thêm các radio button để xử lý caption khi ảnh đã có caption
276
+ caption_handling_label = tk.Label(root, text="If a caption already exists for an image:", font=('Helvetica', 12))
277
+ caption_handling_label.pack(pady=5)
278
+
279
+ # Frame chứa các radio button
280
+ options_frame = tk.Frame(root)
281
+ options_frame.pack(pady=5)
282
+
283
+ # Radio buttons
284
+ overwrite_radio = tk.Radiobutton(options_frame, text="Overwrite existing caption", variable=caption_handling_var, value='overwrite')
285
+ overwrite_radio.pack(side="left", padx=10)
286
+
287
+ append_radio = tk.Radiobutton(options_frame, text="Append to existing caption", variable=caption_handling_var, value='append')
288
+ append_radio.pack(side="left", padx=10)
289
+
290
+ skip_radio = tk.Radiobutton(options_frame, text="Skip images with existing caption", variable=caption_handling_var, value='skip')
291
+ skip_radio.pack(side="left", padx=10)
292
+
293
+ load_config_from_json()
294
+
295
+ prompt_var.trace('w', on_config_change)
296
+ max_new_tokens_var.trace('w', on_config_change)
297
+ temperature_var.trace('w', on_config_change)
298
+ top_k_var.trace('w', on_config_change)
299
+ top_p_var.trace('w', on_config_change)
300
+ precision_var.trace('w', on_config_change)
301
+ thread_count_var.trace('w', on_config_change)
302
+ batch_size_var.trace('w', on_config_change)
303
+ prepend_text_var.trace('w', on_config_change)
304
+ append_text_var.trace('w', on_config_change)
305
+ caption_handling_var.trace('w', on_config_change) # Trace for the caption handling radio buttons
306
+
307
+ max_new_tokens_label = tk.Label(root, text="Max New Tokens (max number of tokens to generate):")
308
+ max_new_tokens_label.pack(pady=5)
309
+ max_new_tokens_entry = tk.Entry(root, textvariable=max_new_tokens_var, justify='center', width=5, validate='key', validatecommand=(validate_cmd, '%P'))
310
+ max_new_tokens_entry.pack(pady=5)
311
+
312
+ do_sample_check = tk.Checkbutton(root, text="Do Sample (random sampling):", variable=do_sample_var, command=toggle_sampling_options)
313
+ do_sample_check.pack(pady=5)
314
+
315
+ temperature_label = tk.Label(root, text="Temperature (control randomness of sampling):")
316
+ top_k_label = tk.Label(root, text="Top-k (consider top k tokens):")
317
+ top_p_label = tk.Label(root, text="Top-p (consider tokens with cumulative probability p):")
318
+
319
+ temperature_entry = tk.Entry(root, textvariable=temperature_var, justify='center', width=5, validate='key', validatecommand=(validate_cmd, '%P'))
320
+ top_k_entry = tk.Entry(root, textvariable=top_k_var, justify='center', width=5, validate='key', validatecommand=(validate_cmd, '%P'))
321
+ top_p_entry = tk.Entry(root, textvariable=top_p_var, justify='center', width=5, validate='key', validatecommand=(validate_cmd, '%P'))
322
+
323
+ # Frame to hold all three horizontally aligned elements
324
+ horizontal_frame = tk.Frame(root)
325
+ horizontal_frame.pack(pady=5, padx=5)
326
+
327
+ thread_count_label = tk.Label(horizontal_frame, text="Thread Count (number of threads to use):")
328
+ thread_count_label.pack(side=tk.LEFT, padx=5)
329
+ thread_count_entry = tk.Entry(horizontal_frame, textvariable=thread_count_var, justify='center', width=5, validate='key', validatecommand=(validate_cmd, '%P'))
330
+ thread_count_entry.pack(side=tk.LEFT, padx=5)
331
+
332
+ batch_size_label = tk.Label(horizontal_frame, text="Batch Size (number of images to process at once):")
333
+ batch_size_label.pack(side=tk.LEFT, padx=5)
334
+ batch_size_entry = tk.Entry(horizontal_frame, textvariable=batch_size_var, justify='center', width=5, validate='key', validatecommand=(validate_cmd, '%P'))
335
+ batch_size_entry.pack(side=tk.LEFT, padx=5)
336
+
337
+ errors_button = tk.Button(root, textvariable=errors_var, command=show_errors)
338
+ errors_button.pack(pady=10)
339
+
340
+ start_button = tk.Button(root, text="Generate Captions", command=lambda: [process_files(), update_and_save_config()])
341
+ start_button.pack(pady=10)
342
+
343
+ stop_button = tk.Button(root, text="Stop", command=stop_processing_func)
344
+ stop_button.pack(pady=10)
345
+
346
+ progress_bar = ttk.Progressbar(root, variable=progress, maximum=100)
347
+ progress_bar.pack(pady=10, fill=tk.X)
348
+
349
+ status_label = tk.Label(root, textvariable=status_var, fg="green")
350
+ status_label.pack(pady=5)
351
+
352
+ center_window(root)
353
+ root.protocol("WM_DELETE_WINDOW", on_closing)
354
+ root.mainloop()
355
+
356
+ def select_files():
357
+ global selected_files, save_directory, total_pages, original_selected_files
358
+ filetypes = [("All Image files", "*.jpg;*.jpeg;*.png;*.gif;*.bmp;*.tiff;*.tif;*.svg;*.webp")]
359
+ filepaths = filedialog.askopenfilenames(title="Select Image Files", filetypes=filetypes)
360
+ if filepaths:
361
+ selected_files.clear()
362
+ selected_files.extend(filepaths)
363
+ original_selected_files = selected_files.copy()
364
+ validate_selected_files()
365
+
366
+ num_files_var.set(f"{len(selected_files)} files selected.")
367
+ save_directory = os.path.dirname(selected_files[0])
368
+ total_pages = (len(selected_files) + images_per_page - 1) // images_per_page
369
+ if caption_window is not None:
370
+ update_image_preview(content_canvas)
371
+
372
+ def validate_selected_files():
373
+ global selected_files, num_files_var
374
+ selected_files = [file for file in selected_files if os.path.exists(file)]
375
+ num_files_var.set(f"{len(selected_files)} files selected.")
376
+
377
+ def toggle_buttons(state):
378
+ state = tk.NORMAL if state else tk.DISABLED
379
+ select_files_button.config(state=state)
380
+ show_captions_button.config(state=state)
381
+ prompt_entry.config(state=state)
382
+ prepend_text_entry.config(state=state)
383
+ append_text_entry.config(state=state)
384
+ do_sample_check.config(state=state)
385
+ temperature_entry.config(state=state)
386
+ top_k_entry.config(state=state)
387
+ top_p_entry.config(state=state)
388
+ thread_count_entry.config(state=state)
389
+ batch_size_entry.config(state=state)
390
+ start_button.config(state=state)
391
+ stop_button.config(state=tk.NORMAL)
392
+
393
+ def generate_caption(image_path, save_directory, q):
394
+ if stop_processing:
395
+ return
396
+
397
+ try:
398
+ load_model()
399
+
400
+ filename = os.path.basename(image_path)
401
+ caption_file_path = os.path.join(save_directory, f"{filename}_caption.txt")
402
+
403
+ # Kiểm tra các lựa chọn của người dùng
404
+ if os.path.exists(caption_file_path):
405
+ if caption_handling_var.get() == 'skip':
406
+ q.put(image_path)
407
+ return
408
+ elif caption_handling_var.get() == 'append':
409
+ with open(caption_file_path, 'r', encoding='utf-8') as f:
410
+ existing_caption = f.read()
411
+ else:
412
+ existing_caption = ""
413
+ else:
414
+ existing_caption = ""
415
+
416
+ image = PILImage.open(image_path).convert('RGB')
417
+ if not isinstance(image, PILImage.Image):
418
+ raise ValueError(f"Expected image to be of type PIL.Image.Image, but got {type(image)}")
419
+
420
+ inputs = model.build_conversation_input_ids(
421
+ tokenizer,
422
+ query=prompt_var.get(),
423
+ history=[],
424
+ images=[image]
425
+ )
426
+ inputs = {
427
+ 'input_ids': inputs['input_ids'].unsqueeze(0).to('cuda'),
428
+ 'token_type_ids': inputs['token_type_ids'].unsqueeze(0).to('cuda'),
429
+ 'attention_mask': inputs['attention_mask'].unsqueeze(0).to('cuda'),
430
+ 'images': [[inputs['images'][0].to('cuda').to(torch.float16)]],
431
+ }
432
+ gen_kwargs = {
433
+ "max_new_tokens": max_new_tokens_var.get(),
434
+ "do_sample": do_sample_var.get(),
435
+ "temperature": temperature_var.get(),
436
+ "top_k": top_k_var.get(),
437
+ "top_p": top_p_var.get() if do_sample_var.get() else None,
438
+ "num_beams": precision_var.get()
439
+ }
440
+
441
+ with torch.no_grad():
442
+ outputs = model.generate(**inputs, **gen_kwargs)
443
+ outputs = outputs[:, inputs['input_ids'].shape[1]:]
444
+ new_caption = tokenizer.decode(outputs[0], skip_special_tokens=True)
445
+
446
+ final_caption = f"{prepend_text_var.get()} {existing_caption} {new_caption} {append_text_var.get()}".strip()
447
+
448
+ with open(caption_file_path, 'w', encoding='utf-8') as file:
449
+ file.write(final_caption)
450
+
451
+ q.put(image_path)
452
+ torch.cuda.empty_cache()
453
+ except torch.cuda.OutOfMemoryError as e:
454
+ torch.cuda.empty_cache()
455
+ error_message = f"CUDA OutOfMemoryError: {traceback.format_exc()}"
456
+ print(error_message)
457
+ q.put(error_message)
458
+ error_messages.append(error_message)
459
+ except Exception as e:
460
+ error_message = f"Error processing image {image_path}: {traceback.format_exc()}"
461
+ print(error_message)
462
+ q.put(error_message)
463
+ error_messages.append(error_message)
464
+
465
+ def worker(save_directory, num_threads, batch_size):
466
+ try:
467
+ progress.set(0)
468
+ threads = []
469
+
470
+ num_batches = math.ceil(len(selected_files) / batch_size)
471
+ batch_size_per_thread = max(1, batch_size // num_threads) # Số ảnh mỗi luồng xử lý trong một batch
472
+
473
+ for batch_index in range(num_batches):
474
+ if stop_processing:
475
+ break
476
+
477
+ start_index = batch_index * batch_size
478
+ end_index = min(start_index + batch_size, len(selected_files))
479
+ batch = selected_files[start_index:end_index]
480
+
481
+ # Chia ảnh trong batch cho các luồng
482
+ for i in range(0, len(batch), batch_size_per_thread):
483
+ thread_batch = batch[i:i + batch_size_per_thread]
484
+ thread = threading.Thread(target=generate_captions_for_batch, args=(thread_batch, save_directory, q))
485
+ threads.append(thread)
486
+ thread.start()
487
+
488
+ # Đợi các luồng trong batch hiện tại hoàn thành
489
+ for thread in threads:
490
+ thread.join()
491
+ threads.clear()
492
+
493
+ q.put(None)
494
+ except Exception as e:
495
+ if not stop_processing:
496
+ q.put(e)
497
+
498
+ def generate_captions_for_batch(batch, save_directory, q):
499
+ for image_path in batch:
500
+ generate_caption(image_path, save_directory, q)
501
+
502
+ def update_progress():
503
+ try:
504
+ completed = 0
505
+ while True:
506
+ item = q.get()
507
+ if item is None:
508
+ break
509
+ if isinstance(item, str):
510
+ if "Error" in item:
511
+ root.after(0, errors_var.set, f"Errors: {len(error_messages)}")
512
+ continue
513
+ completed += 1
514
+ progress.set(int((completed / len(selected_files)) * 100))
515
+ if not stop_processing:
516
+ root.after(0, status_var.set, f"Processed {completed} files")
517
+ root.after(0, root.update_idletasks)
518
+ if not stop_processing:
519
+ root.after(0, progress.set(100))
520
+ show_completion_message(completed)
521
+ except Exception as e:
522
+ if not stop_processing:
523
+ root.after(0, status_var.set(f"Error: {e}"))
524
+ finally:
525
+ toggle_buttons(True)
526
+
527
+ def show_completion_message(completed):
528
+ message = f"Processing complete. {completed} files processed."
529
+ if error_messages:
530
+ message += f" {len(error_messages)} errors occurred."
531
+ messagebox.showinfo("Process Complete", message)
532
+
533
+ def process_files():
534
+ global stop_processing, error_messages
535
+ stop_processing = False
536
+ error_messages.clear()
537
+ errors_var.set("Errors: 0")
538
+
539
+ validate_selected_files()
540
+
541
+ if not selected_files or not save_directory:
542
+ status_var.set("Please select images.")
543
+ return
544
+
545
+ toggle_buttons(False)
546
+
547
+ threading.Thread(target=worker, args=(save_directory, thread_count_var.get(), batch_size_var.get())).start()
548
+ threading.Thread(target=update_progress).start()
549
+
550
+ def stop_processing_func():
551
+ global stop_processing
552
+ stop_processing = True
553
+ torch.cuda.empty_cache()
554
+ status_var.set("Processing stopped.")
555
+
556
+ def open_caption_window():
557
+ global caption_window, caption_frame, caption_text_widgets, current_page, total_pages, content_canvas
558
+ if caption_window is not None:
559
+ return
560
+
561
+ validate_selected_files()
562
+
563
+ caption_window = tk.Toplevel(root)
564
+ caption_window.title("Image Thumbnails and Captions")
565
+ caption_window.geometry("800x900")
566
+
567
+ main_frame = tk.Frame(caption_window)
568
+ main_frame.pack(fill=tk.BOTH, expand=True)
569
+
570
+ search_frame = tk.Frame(main_frame)
571
+ search_frame.pack(side=tk.TOP, fill=tk.X)
572
+
573
+ search_entry = tk.Entry(search_frame, textvariable=search_var)
574
+ search_entry.pack(side=tk.LEFT, padx=10, pady=5, fill=tk.X, expand=True)
575
+
576
+ search_button = tk.Button(search_frame, text="Search", command=search_captions)
577
+ search_button.pack(side=tk.LEFT, padx=10)
578
+
579
+ reset_button = tk.Button(search_frame, text="Reset Order", command=reset_order)
580
+ reset_button.pack(side=tk.LEFT, padx=10)
581
+
582
+ action_frame = tk.Frame(main_frame)
583
+ action_frame.pack(side=tk.TOP, fill=tk.X)
584
+
585
+ action_entry = tk.Entry(action_frame, textvariable=action_var)
586
+ action_entry.pack(side=tk.LEFT, padx=10, pady=5, fill=tk.X, expand=True)
587
+
588
+ prepend_button = tk.Button(action_frame, text="Add to Beginning", command=lambda: add_to_captions("prepend"))
589
+ prepend_button.pack(side=tk.LEFT, padx=5)
590
+
591
+ append_button = tk.Button(action_frame, text="Add to End", command=lambda: add_to_captions("append"))
592
+ append_button.pack(side=tk.LEFT, padx=5)
593
+
594
+ insert_middle_button = tk.Button(action_frame, text="Add to Middle", command=lambda: add_to_captions("insert_middle"))
595
+ insert_middle_button.pack(side=tk.LEFT, padx=5)
596
+
597
+ delete_keyword_button = tk.Button(action_frame, text="Delete Keyword", command=delete_keyword_from_captions)
598
+ delete_keyword_button.pack(side=tk.LEFT, padx=5)
599
+
600
+ delete_images_button = tk.Button(action_frame, text="Delete Images with Keyword", command=delete_images_with_keyword)
601
+ delete_images_button.pack(side=tk.LEFT, padx=5)
602
+
603
+ content_canvas = tk.Canvas(main_frame)
604
+ content_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
605
+
606
+ caption_frame = tk.Frame(content_canvas)
607
+ content_canvas.create_window((0, 0), window=caption_frame, anchor='nw')
608
+
609
+ caption_scrollbar = tk.Scrollbar(main_frame, orient="vertical", command=content_canvas.yview)
610
+ caption_scrollbar.pack(side=tk.LEFT, fill=tk.Y)
611
+ content_canvas.configure(yscrollcommand=caption_scrollbar.set)
612
+
613
+ caption_frame.bind("<Configure>", lambda e: content_canvas.configure(scrollregion=content_canvas.bbox("all")))
614
+ content_canvas.bind_all("<MouseWheel>", lambda event: content_canvas.yview_scroll(int(-1*(event.delta/120)), "units"))
615
+
616
+ def on_caption_window_close():
617
+ global caption_window
618
+ caption_window.destroy()
619
+ caption_window = None
620
+
621
+ caption_window.protocol("WM_DELETE_WINDOW", on_caption_window_close)
622
+
623
+ update_image_preview(content_canvas)
624
+
625
+ def update_image_preview(content_canvas):
626
+ global thumbnails, caption_text_widgets, current_page, images_per_page, total_pages
627
+ if caption_frame is None:
628
+ return
629
+
630
+ for widget in caption_frame.winfo_children():
631
+ if isinstance(widget, tk.Label) or isinstance(widget, tk.Text) or isinstance(widget, tk.Frame):
632
+ widget.destroy()
633
+
634
+ thumbnails.clear()
635
+ caption_text_widgets.clear()
636
+
637
+ if not selected_files:
638
+ return
639
+
640
+ start_index = current_page * images_per_page
641
+ end_index = start_index + images_per_page
642
+ files_to_display = selected_files[start_index:end_index]
643
+
644
+ for i, file_path in enumerate(files_to_display):
645
+ thumbnail_size = (200, 200)
646
+ try:
647
+ image = PILImage.open(file_path)
648
+ image.thumbnail(thumbnail_size)
649
+ thumbnail = ImageTk.PhotoImage(image)
650
+ thumbnails.append(thumbnail)
651
+
652
+ img_label = tk.Label(caption_frame, image=thumbnail)
653
+ img_label.grid(row=i*2, column=0, padx=5, pady=5, sticky="nsew")
654
+
655
+ file_label = tk.Label(caption_frame, text=os.path.basename(file_path), font=('Helvetica', 12))
656
+ file_label.grid(row=i*2, column=1, padx=5, pady=5, sticky="nsew")
657
+
658
+ caption_file = os.path.join(save_directory, f"{os.path.basename(file_path)}_caption.txt")
659
+ if os.path.exists(caption_file):
660
+ with open(caption_file, 'r', encoding='utf-8') as file:
661
+ caption_text = file.read()
662
+ else:
663
+ caption_text = ""
664
+
665
+ caption_var = tk.StringVar(value=caption_text)
666
+
667
+ caption_text_widget = tk.Text(caption_frame, width=50, height=3, wrap=tk.WORD, font=('Helvetica', 12))
668
+ caption_text_widget.insert(tk.END, caption_text)
669
+ caption_text_widget.grid(row=i*2, column=2, padx=5, pady=5, sticky="nsew")
670
+
671
+ caption_var.trace_add("write", lambda *args, fp=file_path, cv=caption_var: save_caption(fp, cv.get()))
672
+
673
+ caption_text_widget.bind("<KeyRelease>", lambda e, cv=caption_var, w=caption_text_widget: cv.set(w.get("1.0", "end-1c")))
674
+ caption_text_widgets.append(caption_text_widget)
675
+
676
+ except Exception as e:
677
+ tk.Label(caption_frame, text="Error loading image").grid(row=i*2, column=0, columnspan=4, padx=5, pady=5)
678
+
679
+ nav_frame = tk.Frame(caption_frame)
680
+ nav_frame.grid(row=images_per_page*2, column=0, columnspan=3, pady=10)
681
+
682
+ if current_page > 0:
683
+ prev_button = tk.Button(nav_frame, text="Previous", command=lambda: navigate(-1, content_canvas))
684
+ prev_button.pack(side=tk.LEFT)
685
+
686
+ page_label = tk.Label(nav_frame, text=f"Page {current_page + 1} of {total_pages}")
687
+ page_label.pack(side=tk.LEFT, padx=5)
688
+
689
+ page_entry = tk.Entry(nav_frame, width=5)
690
+ page_entry.pack(side=tk.LEFT)
691
+
692
+ go_button = tk.Button(nav_frame, text="Go", command=lambda: go_to_page(page_entry.get(), content_canvas))
693
+ go_button.pack(side=tk.LEFT, padx=5)
694
+
695
+ if current_page < total_pages - 1:
696
+ next_button = tk.Button(nav_frame, text="Next", command=lambda: navigate(1, content_canvas))
697
+ next_button.pack(side=tk.RIGHT)
698
+
699
+ def navigate(direction, content_canvas):
700
+ global current_page
701
+ current_page += direction
702
+ update_image_preview(content_canvas)
703
+
704
+ def go_to_page(page_number, content_canvas):
705
+ global current_page, total_pages
706
+ try:
707
+ page_number = int(page_number)
708
+ if 1 <= page_number <= total_pages:
709
+ current_page = page_number - 1
710
+ update_image_preview(content_canvas)
711
+ else:
712
+ messagebox.showerror("Invalid Page", f"Please enter a valid page number between 1 and {total_pages}.")
713
+ except ValueError:
714
+ messagebox.showerror("Invalid Input", "Please enter a valid integer for the page number.")
715
+
716
+ def save_caption(file_path, caption_text):
717
+ output_path = os.path.join(save_directory, f"{os.path.basename(file_path)}_caption.txt")
718
+ try:
719
+ with open(output_path, 'w', encoding='utf-8') as file:
720
+ file.write(caption_text.strip())
721
+ except Exception as e:
722
+ print(f"Error saving captions: {e}")
723
+
724
+ def search_captions():
725
+ global selected_files
726
+ search_term = search_var.get().lower().strip()
727
+ if not search_term:
728
+ return
729
+
730
+ try:
731
+ selected_files.sort(key=lambda x: search_score(x, search_term), reverse=True)
732
+ except Exception as e:
733
+ error_message = f"Error during sorting: {e}"
734
+ print(error_message)
735
+ error_messages.append(error_message)
736
+
737
+ update_image_preview(content_canvas)
738
+
739
+ def search_score(file_path, search_term):
740
+ caption_file = os.path.join(save_directory, f"{os.path.basename(file_path)}_caption.txt")
741
+ try:
742
+ if os.path.exists(caption_file):
743
+ with open(caption_file, 'r', encoding='utf-8') as file:
744
+ caption_text = file.read().lower()
745
+ if search_term in caption_text:
746
+ return caption_text.count(search_term)
747
+
748
+ except Exception as e:
749
+ error_message = f"Error reading file {caption_file}: {e}"
750
+ print(error_message)
751
+ error_messages.append(error_message)
752
+ return 0
753
+
754
+ def reset_order():
755
+ global selected_files
756
+ selected_files = original_selected_files.copy()
757
+ update_image_preview(content_canvas)
758
+
759
+ def add_to_captions(position):
760
+ global selected_files
761
+ keyword = action_var.get()
762
+ if not keyword:
763
+ return
764
+
765
+ for file_path in selected_files:
766
+ caption_file = os.path.join(save_directory, f"{os.path.basename(file_path)}_caption.txt")
767
+ if os.path.exists(caption_file):
768
+ with open(caption_file, 'r+', encoding='utf-8') as file:
769
+ caption_text = file.read()
770
+ if position == "prepend":
771
+ caption_text = f"{keyword} {caption_text}"
772
+ elif position == "append":
773
+ caption_text = f"{caption_text} {keyword}"
774
+ elif position == "insert_middle":
775
+ middle_index = len(caption_text) // 2
776
+ caption_text = f"{caption_text[:middle_index]} {keyword} {caption_text[middle_index:]}"
777
+ file.seek(0)
778
+ file.write(caption_text)
779
+ file.truncate()
780
+
781
+ update_image_preview(content_canvas)
782
+
783
+ def delete_keyword_from_captions():
784
+ keyword = action_var.get().lower().strip()
785
+ if not keyword:
786
+ return
787
+
788
+ for file_path in selected_files:
789
+ caption_file = os.path.join(save_directory, f"{os.path.basename(file_path)}_caption.txt")
790
+ if os.path.exists(caption_file):
791
+ with open(caption_file, 'r+', encoding='utf-8') as file:
792
+ caption_text = file.read().lower().replace(keyword, "")
793
+
794
+ updated_caption = caption_text.replace(keyword, "").strip()
795
+
796
+ file.seek(0)
797
+ file.write(updated_caption)
798
+ file.truncate()
799
+
800
+ update_image_preview(content_canvas)
801
+
802
+ def delete_images_with_keyword():
803
+ global selected_files
804
+ keyword = action_var.get().lower()
805
+ if not keyword:
806
+ return
807
+
808
+ files_to_delete = []
809
+ for file_path in selected_files:
810
+ caption_file = os.path.join(save_directory, f"{os.path.basename(file_path)}_caption.txt")
811
+ if os.path.exists(caption_file):
812
+ with open(caption_file, 'r', encoding='utf-8') as file:
813
+ caption_text = file.read().lower()
814
+ if keyword in caption_text:
815
+ files_to_delete.append(file_path)
816
+
817
+ for file_path in files_to_delete:
818
+ try:
819
+ os.remove(file_path)
820
+ caption_file = os.path.join(save_directory, f"{os.path.basename(file_path)}_caption.txt")
821
+ if os.path.exists(caption_file):
822
+ os.remove(caption_file)
823
+ except Exception as e:
824
+ error_message = f"Error deleting file {file_path} or its caption: {e}"
825
+ print(error_message)
826
+ error_messages.append(error_message)
827
+
828
+ selected_files = [file_path for file_path in selected_files if file_path not in files_to_delete]
829
+
830
+ validate_selected_files()
831
+
832
+ update_image_preview(content_canvas)
833
+
834
+ def return_to_menu():
835
+ stop_processing_func()
836
+ root.destroy()
837
+ import main
838
+ main.open_main_menu()
839
+
840
+ def on_closing():
841
+ return_to_menu()
842
+
843
+ if __name__ == "__main__":
844
+ open_image_to_caption()
image_to_tag.py ADDED
@@ -0,0 +1,784 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tkinter as tk
2
+ from tkinter import filedialog, messagebox, ttk
3
+ from PIL import Image as PILImage, ImageTk
4
+ import os
5
+ import queue
6
+ import threading
7
+ import subprocess
8
+ import sys
9
+ import json
10
+
11
+ # Global variables to control the process and track errors
12
+ stop_processing = False
13
+ error_messages = []
14
+ selected_files = []
15
+ save_directory = "" # Sử dụng thư mục của ảnh đầu tiên được chọn
16
+ caption_window = None
17
+ caption_frame = None
18
+ thumbnails = [] # To hold references to thumbnail objects
19
+ caption_text_widgets = [] # To hold Text widgets containing captions
20
+ tag_dict = {} # Store tags and their counts
21
+ selected_tag = None # Store the selected tag widget
22
+ edit_buttons = {} # To hold the edit and delete buttons for each tag
23
+ error_window = None # To ensure only one error window is open at a time
24
+ tag_text_frame = None # Define globally to manage tag frame
25
+
26
+ # Global Tkinter variable placeholders
27
+ status_var = None
28
+ num_files_var = None
29
+ errors_var = None
30
+ progress = None
31
+ character_threshold_var = None
32
+ general_threshold_var = None
33
+ thread_count_var = None
34
+ batch_size_var = None
35
+ start_button = None
36
+ stop_button = None
37
+ prepend_text_var = None
38
+ append_text_var = None
39
+ current_page = 0
40
+ images_per_page = 20
41
+ total_pages = 1 # Initialize total pages
42
+
43
+ def update_and_save_config():
44
+ """Update and save the configuration to JSON."""
45
+ save_config_to_json(
46
+ model="swinv2", # Default model
47
+ general_threshold=general_threshold_var.get(),
48
+ character_threshold=character_threshold_var.get(),
49
+ model_dir="D:/test/models/wd-swinv2-tagger-v3" # Default model directory
50
+ )
51
+
52
+ def show_errors(root):
53
+ """Display error details in a new window."""
54
+ global error_window
55
+ if error_window is not None:
56
+ return # Only one error window can be open at a time
57
+
58
+ error_window = tk.Toplevel(root)
59
+ error_window.title("Error Details")
60
+ error_window.geometry("500x400")
61
+
62
+ error_text = tk.Text(error_window, wrap='word')
63
+ error_text.pack(expand=True, fill='both')
64
+
65
+ if error_messages:
66
+ for error in error_messages:
67
+ error_text.insert('end', error + '\n')
68
+ else:
69
+ error_text.insert('end', "No errors recorded.")
70
+
71
+ error_text.config(state='disabled')
72
+
73
+ def on_close_error_window():
74
+ global error_window
75
+ error_window.destroy()
76
+ error_window = None
77
+
78
+ error_window.protocol("WM_DELETE_WINDOW", on_close_error_window)
79
+
80
+ def save_config_to_json(model, general_threshold, character_threshold, model_dir, filepath='config.json'):
81
+ """Save the model and threshold values to a JSON file."""
82
+ config = {
83
+ 'model': model,
84
+ 'general_threshold': general_threshold,
85
+ 'character_threshold': character_threshold,
86
+ 'model_dir': model_dir
87
+ }
88
+ try:
89
+ with open(filepath, 'w') as f:
90
+ json.dump(config, f, indent=2)
91
+ except Exception as e:
92
+ print(f"Error saving config to JSON: {e}")
93
+
94
+ def open_image_to_tag():
95
+ global stop_processing, error_messages, selected_files, save_directory, caption_window, caption_frame, thumbnails, caption_text_widgets, tag_dict, selected_tag, edit_buttons, tag_text_frame, current_page, total_pages, content_canvas
96
+ global status_var, num_files_var, errors_var, progress, character_threshold_var, general_threshold_var, thread_count_var, batch_size_var
97
+ global start_button, stop_button, prepend_text_var, append_text_var
98
+
99
+ # Create Tkinter window
100
+ root = tk.Tk()
101
+ root.title("Image Caption Generator")
102
+
103
+ # Initialize Tkinter variables
104
+ status_var = tk.StringVar()
105
+ num_files_var = tk.StringVar()
106
+ errors_var = tk.StringVar(value="Errors: 0")
107
+ progress = tk.IntVar()
108
+ character_threshold_var = tk.DoubleVar(value=0.35)
109
+ general_threshold_var = tk.DoubleVar(value=0.35)
110
+ thread_count_var = tk.IntVar(value=4)
111
+ batch_size_var = tk.IntVar(value=4)
112
+ prepend_text_var = tk.StringVar()
113
+ append_text_var = tk.StringVar()
114
+ q = queue.Queue()
115
+
116
+ def center_window(window, width_extra=0, height_extra=0):
117
+ window.update_idletasks()
118
+ width = 100 + width_extra
119
+ height = 820 + height_extra
120
+ x = (window.winfo_screenwidth() // 2) - (width // 2)
121
+ y = (window.winfo_screenheight() // 2) - (height // 2)
122
+ window.geometry(f'{width}x{height}+{x}+{y}')
123
+
124
+ def select_files():
125
+ global selected_files, save_directory, total_pages
126
+ filetypes = [
127
+ ("All Image files", "*.jpg;*.jpeg;*.png;*.gif;*.bmp;*.tiff;*.tif;*.svg;*.webp"),
128
+ ("JPEG files", "*.jpg;*.jpeg"),
129
+ ("PNG files", "*.png"),
130
+ ("GIF files", "*.gif"),
131
+ ("BMP files", "*.bmp"),
132
+ ("TIFF files", "*.tiff;*.tif"),
133
+ ("SVG files", "*.svg"),
134
+ ("WEBP files", "*.webp")
135
+ ]
136
+ filepaths = filedialog.askopenfilenames(title="Select Image Files", filetypes=filetypes)
137
+ if filepaths:
138
+ selected_files.clear()
139
+ selected_files.extend(filepaths)
140
+ num_files_var.set(f"{len(selected_files)} files selected.")
141
+ save_directory = os.path.dirname(selected_files[0]) # Lấy thư mục của ảnh đầu tiên
142
+ total_pages = (len(selected_files) + images_per_page - 1) // images_per_page # Cập nhật tổng số trang
143
+ if caption_window is not None:
144
+ update_image_preview(content_canvas)
145
+
146
+ def toggle_buttons(state):
147
+ """Enable or disable buttons based on the state."""
148
+ state = tk.NORMAL if state else tk.DISABLED
149
+ select_files_button.config(state=state)
150
+ show_captions_button.config(state=state)
151
+ general_threshold_entry.config(state=state)
152
+ character_threshold_entry.config(state=state)
153
+ thread_count_entry.config(state=state)
154
+ batch_size_entry.config(state=state)
155
+ prepend_text_entry.config(state=state)
156
+ append_text_entry.config(state=state)
157
+ start_button.config(state=state)
158
+ # Stop button should always be enabled
159
+ stop_button.config(state=tk.NORMAL)
160
+
161
+ def generate_caption(image_path, save_directory, q):
162
+ """Generate captions for a single image using the wd-swinv2-tagger-v3 model."""
163
+ if stop_processing:
164
+ return
165
+
166
+ try:
167
+ filename = os.path.basename(image_path)
168
+ output_path = os.path.join(save_directory, f"{filename}_tags.txt")
169
+
170
+ command = [
171
+ sys.executable, 'D:/test/wdv3-timm-main/wdv3_timm.py',
172
+ '--model', "swinv2",
173
+ '--image_path', image_path,
174
+ '--model_dir', "D:/test/models/wd-swinv2-tagger-v3",
175
+ '--general_threshold', str(general_threshold_var.get()),
176
+ '--character_threshold', str(character_threshold_var.get())
177
+ ]
178
+
179
+ result = subprocess.run(command, capture_output=True, text=True, check=True)
180
+ output = result.stdout
181
+ print(output) # Debug: In ra toàn bộ đầu ra từ lệnh subprocess
182
+ error_output = result.stderr
183
+ print(output) # In ra đầu ra từ lệnh subprocess
184
+ print(error_output) # In ra đầu ra lỗi từ lệnh subprocess
185
+
186
+ # Filter out information to contain only Caption or General tags
187
+ filtered_output = []
188
+ recording = False
189
+ for line in output.split('\n'):
190
+ if "General tags" in line:
191
+ recording = True
192
+ continue
193
+ if recording:
194
+ if line.startswith(' '):
195
+ tag = line.strip().split(':')[0].replace('_', ' ')
196
+ filtered_output.append(tag)
197
+ else:
198
+ recording = False
199
+ break
200
+
201
+ # Convert list of tags to comma-separated string
202
+ final_tags = ','.join(filtered_output) if filtered_output else "No tags found"
203
+ print("Filtered output:", final_tags) # Debug: In ra các nhãn cuối cùng sau khi lọc
204
+
205
+ # Add prepend and append text
206
+ final_tags = f"{prepend_text_var.get()},{final_tags},{append_text_var.get()}".strip(',')
207
+
208
+ # Save result to text file
209
+ with open(output_path, 'w', encoding='utf-8') as file:
210
+ file.write(final_tags)
211
+
212
+ q.put(image_path)
213
+ except Exception as e:
214
+ error_message = f"Error processing image {image_path}: {str(e)}"
215
+ print(error_message) # Debug: In ra thông báo lỗi
216
+ q.put(error_message)
217
+ error_messages.append(error_message)
218
+
219
+ def worker(save_directory, num_threads, batch_size):
220
+ try:
221
+ progress.set(0)
222
+ batch = []
223
+ threads = []
224
+ for i, input_path in enumerate(selected_files, 1):
225
+ if stop_processing:
226
+ break
227
+
228
+ batch.append(input_path)
229
+ if len(batch) == batch_size or i == len(selected_files):
230
+ for image_path in batch:
231
+ thread = threading.Thread(target=generate_caption, args=(image_path, save_directory, q))
232
+ threads.append(thread)
233
+ thread.start()
234
+
235
+ for thread in threads:
236
+ thread.join()
237
+
238
+ batch.clear()
239
+ threads.clear()
240
+
241
+ q.put(None)
242
+ except Exception as e:
243
+ if not stop_processing:
244
+ q.put(e)
245
+
246
+ def update_progress():
247
+ try:
248
+ completed = 0
249
+ while True:
250
+ item = q.get()
251
+ if item is None:
252
+ break
253
+ if isinstance(item, str):
254
+ if "Error" in item:
255
+ root.after(0, errors_var.set, f"Errors: {len(error_messages)}")
256
+ continue
257
+ completed += 1
258
+ progress.set(int((completed / len(selected_files)) * 100))
259
+ if not stop_processing:
260
+ root.after(0, status_var.set, f"Processed {completed} files")
261
+ root.after(0, root.update_idletasks)
262
+ if caption_window is not None:
263
+ root.after(0, update_image_preview, content_canvas)
264
+ if not stop_processing:
265
+ root.after(0, progress.set(100))
266
+ show_completion_message(completed)
267
+ except Exception as e:
268
+ if not stop_processing:
269
+ root.after(0, status_var.set, f"Error: {e}")
270
+ finally:
271
+ # Re-enable buttons after processing completes
272
+ toggle_buttons(True)
273
+
274
+ def show_completion_message(completed):
275
+ message = f"Processing complete. {completed} files processed."
276
+ if error_messages:
277
+ message += f" {len(error_messages)} errors occurred."
278
+ messagebox.showinfo("Process Complete", message)
279
+
280
+ def process_files():
281
+ global stop_processing, error_messages
282
+ stop_processing = False
283
+ error_messages.clear()
284
+ errors_var.set("Errors: 0")
285
+ if not selected_files or not save_directory:
286
+ status_var.set("Please select images.")
287
+ return
288
+
289
+ # Disable buttons during processing
290
+ toggle_buttons(False)
291
+
292
+ threading.Thread(target=worker, args=(save_directory, thread_count_var.get(), batch_size_var.get())).start()
293
+ threading.Thread(target=update_progress).start()
294
+
295
+ def stop_processing_func():
296
+ global stop_processing
297
+ stop_processing = True
298
+ status_var.set("Processing stopped.")
299
+
300
+ def return_to_menu():
301
+ stop_processing_func()
302
+ root.destroy()
303
+ import main
304
+ main.open_main_menu()
305
+
306
+ def on_closing():
307
+ # Save all captions before closing
308
+ save_all_captions()
309
+ return_to_menu()
310
+
311
+ def validate_threshold(P):
312
+ """Validate that the threshold value is between 0 and 1."""
313
+ if P == "" or (P.replace('.', '', 1).isdigit() and 0 <= float(P) <= 1):
314
+ return True
315
+ return False
316
+
317
+ def validate_thread_count(P):
318
+ """Validate that the thread count is a positive integer."""
319
+ if P == "" or P.isdigit() and int(P) > 0:
320
+ return True
321
+ return False
322
+
323
+ def validate_batch_size(P):
324
+ """Validate that the batch size is a positive integer."""
325
+ if P == "" or P.isdigit() and int(P) > 0:
326
+ return True
327
+ return False
328
+
329
+ def open_caption_window():
330
+ global caption_window, caption_frame, caption_text_widgets, tag_text_frame, current_page, total_pages, content_canvas
331
+ if caption_window is not None:
332
+ return # A caption window is already open, do not create a new one
333
+
334
+ caption_window = tk.Toplevel(root)
335
+ caption_window.title("Image Thumbnails and Captions")
336
+ caption_window.geometry("1400x900") # Điều chỉnh kích thước cửa sổ
337
+
338
+ # Create main frame to hold canvas and other elements
339
+ main_frame = tk.Frame(caption_window)
340
+ main_frame.pack(fill=tk.BOTH, expand=True)
341
+
342
+ # Create canvas for content
343
+ content_canvas = tk.Canvas(main_frame)
344
+ content_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
345
+
346
+ # Create frame for content
347
+ caption_frame = tk.Frame(content_canvas)
348
+ content_canvas.create_window((0, 0), window=caption_frame, anchor='nw')
349
+
350
+ # Add scrollbar for caption_frame
351
+ caption_scrollbar = tk.Scrollbar(main_frame, orient="vertical", command=content_canvas.yview)
352
+ caption_scrollbar.pack(side=tk.LEFT, fill=tk.Y)
353
+ content_canvas.configure(yscrollcommand=caption_scrollbar.set)
354
+
355
+ caption_frame.bind("<Configure>", lambda e: content_canvas.configure(scrollregion=content_canvas.bbox("all")))
356
+
357
+ # Ensure caption_window is reset to None when the window is closed
358
+ def on_caption_window_close():
359
+ global caption_window, tag_text_frame
360
+ caption_window.destroy()
361
+ caption_window = None
362
+ tag_text_frame = None # Reset tag_text_frame when window is closed
363
+
364
+ caption_window.protocol("WM_DELETE_WINDOW", on_caption_window_close)
365
+
366
+ # Display content and set dividers
367
+ update_image_preview(content_canvas)
368
+
369
+ # Create frame for tags (on the right side)
370
+ tag_frame = tk.Frame(main_frame, padx=0)
371
+ tag_frame.pack(side=tk.RIGHT, fill=tk.Y)
372
+
373
+ # Create sorting buttons
374
+ sort_label = tk.Label(tag_frame, text="Sort by:", font=('Helvetica', 12))
375
+ sort_label.pack(anchor='nw', pady=5)
376
+
377
+ sort_alpha_button = tk.Button(tag_frame, text="Alphabetical", command=sort_tags_alpha)
378
+ sort_alpha_button.pack(anchor='nw', pady=2)
379
+
380
+ sort_count_button = tk.Button(tag_frame, text="Count", command=sort_tags_count)
381
+ sort_count_button.pack(anchor='nw', pady=2)
382
+
383
+ # Create display area for tags
384
+ tag_canvas = tk.Canvas(tag_frame)
385
+ tag_text_frame = tk.Frame(tag_canvas)
386
+ tag_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
387
+ tag_canvas.create_window((0, 0), window=tag_text_frame, anchor='nw')
388
+
389
+ # Add scrollbar for tag_frame
390
+ tag_scrollbar = tk.Scrollbar(main_frame, orient="vertical", command=tag_canvas.yview)
391
+ tag_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
392
+ tag_canvas.configure(yscrollcommand=tag_scrollbar.set)
393
+
394
+ tag_text_frame.bind("<Configure>", lambda e: tag_canvas.configure(scrollregion=tag_canvas.bbox("all")))
395
+
396
+ # Update initial tag display
397
+ update_tag_display()
398
+
399
+ def update_image_preview(content_canvas):
400
+ global thumbnails, caption_text_widgets, current_page, images_per_page, total_pages
401
+ if caption_frame is None:
402
+ return
403
+
404
+ for widget in caption_frame.winfo_children():
405
+ if isinstance(widget, tk.Label) or isinstance(widget, tk.Text) or isinstance(widget, tk.Frame):
406
+ widget.destroy()
407
+
408
+ thumbnails.clear() # Clear old thumbnails
409
+ caption_text_widgets.clear() # Clear old caption_entries
410
+
411
+ if not selected_files:
412
+ return
413
+
414
+ start_index = current_page * images_per_page
415
+ end_index = start_index + images_per_page
416
+ files_to_display = selected_files[start_index:end_index]
417
+
418
+ for i, file_path in enumerate(files_to_display):
419
+ thumbnail_size = (200, 200)
420
+ try:
421
+ image = PILImage.open(file_path)
422
+ image.thumbnail(thumbnail_size)
423
+ thumbnail = ImageTk.PhotoImage(image)
424
+ thumbnails.append(thumbnail) # Keep reference to thumbnail object
425
+
426
+ # Display thumbnail
427
+ img_label = tk.Label(caption_frame, image=thumbnail)
428
+ img_label.grid(row=i*2, column=0, padx=5, pady=5, sticky="nsew")
429
+
430
+ # Display file name
431
+ file_label = tk.Label(caption_frame, text=os.path.basename(file_path), font=('Helvetica', 12))
432
+ file_label.grid(row=i*2, column=1, padx=5, pady=5, sticky="nsew")
433
+
434
+ # Check and display caption if available
435
+ caption_file = os.path.join(save_directory, f"{os.path.basename(file_path)}_tags.txt")
436
+ if os.path.exists(caption_file):
437
+ with open(caption_file, 'r', encoding='utf-8') as file:
438
+ caption_text = file.read()
439
+ else:
440
+ caption_text = "" # Set empty string if no caption available
441
+
442
+ caption_text_widget = tk.Text(caption_frame, width=50, height=3, wrap=tk.WORD, font=('Helvetica', 12))
443
+ caption_text_widget.insert(tk.END, caption_text)
444
+ caption_text_widget.grid(row=i*2, column=2, padx=5, pady=5, sticky="nsew")
445
+ caption_text_widget.bind("<FocusOut>", lambda e, fp=file_path: save_caption(fp, caption_text_widget.get("1.0", "end-1c")))
446
+ caption_text_widgets.append(caption_text_widget)
447
+
448
+ # Update tags in tag_dict
449
+ update_tag_dict(caption_text)
450
+
451
+ except Exception as e:
452
+ tk.Label(caption_frame, text="Error loading image").grid(row=i*2, column=0, columnspan=4, padx=5, pady=5)
453
+
454
+ # Add navigation buttons
455
+ nav_frame = tk.Frame(caption_frame)
456
+ nav_frame.grid(row=images_per_page*2, column=0, columnspan=3, pady=10)
457
+
458
+ if current_page > 0:
459
+ prev_button = tk.Button(nav_frame, text="Previous", command=lambda: navigate(-1, content_canvas))
460
+ prev_button.pack(side=tk.LEFT)
461
+
462
+ page_label = tk.Label(nav_frame, text=f"Page {current_page + 1} of {total_pages}")
463
+ page_label.pack(side=tk.LEFT, padx=5)
464
+
465
+ page_entry = tk.Entry(nav_frame, width=5)
466
+ page_entry.pack(side=tk.LEFT)
467
+
468
+ go_button = tk.Button(nav_frame, text="Go", command=lambda: go_to_page(page_entry.get(), content_canvas))
469
+ go_button.pack(side=tk.LEFT, padx=5)
470
+
471
+ if current_page < total_pages - 1:
472
+ next_button = tk.Button(nav_frame, text="Next", command=lambda: navigate(1, content_canvas))
473
+ next_button.pack(side=tk.RIGHT)
474
+
475
+ # Update tag display after loading images
476
+ update_tag_display()
477
+
478
+ def navigate(direction, content_canvas):
479
+ global current_page
480
+ current_page += direction
481
+ update_image_preview(content_canvas)
482
+
483
+ def go_to_page(page_number, content_canvas):
484
+ global current_page, total_pages
485
+ try:
486
+ page_number = int(page_number)
487
+ if 1 <= page_number <= total_pages:
488
+ current_page = page_number - 1
489
+ update_image_preview(content_canvas)
490
+ else:
491
+ messagebox.showerror("Invalid Page", f"Please enter a valid page number between 1 and {total_pages}.")
492
+ except ValueError:
493
+ messagebox.showerror("Invalid Input", "Please enter a valid integer for the page number.")
494
+
495
+ def save_caption(file_path, caption_text):
496
+ """Save caption when user changes it."""
497
+ output_path = os.path.join(save_directory, f"{os.path.basename(file_path)}_tags.txt")
498
+ with open(output_path, 'w', encoding='utf-8') as file:
499
+ file.write(caption_text)
500
+
501
+ # Update tags after editing caption
502
+ update_tag_dict(caption_text)
503
+ update_tag_display()
504
+
505
+ def save_all_captions():
506
+ """Save all captions when closing the window."""
507
+ for i, caption_text_widget in enumerate(caption_text_widgets):
508
+ if caption_text_widget.winfo_exists():
509
+ file_path = selected_files[current_page * images_per_page + i]
510
+ caption_text = caption_text_widget.get("1.0", "end-1c")
511
+ save_caption(file_path, caption_text)
512
+
513
+ def update_tag_dict(caption_text=None):
514
+ """Update tag dictionary based on caption_text."""
515
+ global tag_dict
516
+ new_tag_dict = {}
517
+ for caption_widget in caption_text_widgets:
518
+ if caption_widget.winfo_exists():
519
+ caption_text = caption_widget.get("1.0", "end-1c")
520
+ tags = caption_text.split(',')
521
+ for tag in tags:
522
+ tag = tag.strip()
523
+ if tag:
524
+ if tag in new_tag_dict:
525
+ new_tag_dict[tag] += 1
526
+ else:
527
+ new_tag_dict[tag] = 1
528
+ tag_dict = new_tag_dict
529
+
530
+ def update_tag_display():
531
+ """Update tag display on Text widget."""
532
+ global tag_dict, selected_tag, edit_buttons, tag_text_frame
533
+ if tag_text_frame is None:
534
+ return # If the tag_text_frame is not created yet, return
535
+ for widget in tag_text_frame.winfo_children():
536
+ widget.destroy()
537
+
538
+ row = 0
539
+ for tag, count in tag_dict.items():
540
+ tag_label = tk.Label(tag_text_frame, text=tag, bg="white", padx=5, pady=2, anchor="w", relief="solid", borderwidth=1)
541
+ tag_label.grid(row=row, column=0, sticky="w", padx=2, pady=1)
542
+ count_label = tk.Label(tag_text_frame, text=f"({count})", anchor='w', width=5)
543
+ count_label.grid(row=row, column=1, padx=5, pady=2, sticky='w')
544
+
545
+ # Add tag manipulation buttons
546
+ add_tag_button = tk.Button(tag_text_frame, text="Add", command=lambda r=row: add_tag(r))
547
+ add_tag_button.grid(row=row, column=2, padx=2)
548
+ edit_button = tk.Button(tag_text_frame, text="Edit", command=lambda t=tag_label: edit_tag(t))
549
+ edit_button.grid(row=row, column=3, padx=2)
550
+ delete_button = tk.Button(tag_text_frame, text="Delete", command=lambda t=tag_label: delete_tag(t))
551
+ delete_button.grid(row=row, column=4, padx=2)
552
+ delete_all_button = tk.Button(tag_text_frame, text="Delete All", command=lambda t=tag_label: delete_all_files_with_tag(t))
553
+ delete_all_button.grid(row=row, column=5, padx=2)
554
+
555
+ edit_buttons[tag_label] = [add_tag_button, edit_button, delete_button, delete_all_button]
556
+
557
+ row += 1
558
+
559
+ # Điều chỉnh trọng số cột để không mở rộng quá mức
560
+ tag_text_frame.grid_columnconfigure(0, weight=1) # Cột cho tag
561
+ tag_text_frame.grid_columnconfigure(1, weight=0) # Cột cho số lượng
562
+ tag_text_frame.grid_columnconfigure(2, weight=0) # Cột cho nút Add
563
+ tag_text_frame.grid_columnconfigure(3, weight=0) # Cột cho nút Edit
564
+ tag_text_frame.grid_columnconfigure(4, weight=0) # Cột cho nút Delete
565
+ tag_text_frame.grid_columnconfigure(5, weight=0) # Cột cho nút Delete All
566
+
567
+ tag_text_frame.update_idletasks() # Làm mới giao diện sau khi cập nhật danh sách tag
568
+ tag_text_frame.update() # Làm mới giao diện ngay lập tức
569
+
570
+ def add_tag(row):
571
+ """Add a new tag above the selected row."""
572
+ def save_new_tag(event=None):
573
+ new_tag = entry.get().strip()
574
+ if new_tag:
575
+ # Add the new tag to the tag_dict
576
+ if new_tag in tag_dict:
577
+ tag_dict[new_tag] += 1
578
+ else:
579
+ tag_dict[new_tag] = 1
580
+
581
+ # Update the captions in the respective files
582
+ for file_path in selected_files:
583
+ caption_file = os.path.join(save_directory, f"{os.path.basename(file_path)}_tags.txt")
584
+ if os.path.exists(caption_file):
585
+ with open(caption_file, 'r', encoding='utf-8') as file:
586
+ caption_text = file.read()
587
+ new_caption_text = caption_text + ',' + new_tag if caption_text else new_tag
588
+ with open(caption_file, 'w', encoding='utf-8') as file:
589
+ file.write(new_caption_text)
590
+
591
+ update_image_preview(content_canvas) # Update image preview
592
+ update_tag_display() # Update the display of tags
593
+
594
+ # Create entry above the row
595
+ for widget in tag_text_frame.grid_slaves():
596
+ if int(widget.grid_info()["row"]) >= row:
597
+ widget.grid_configure(row=int(widget.grid_info()["row"]) + 1)
598
+
599
+ entry = tk.Entry(tag_text_frame)
600
+ entry.grid(row=row, column=0, sticky="w", padx=2, pady=1)
601
+ entry.bind("<Return>", save_new_tag)
602
+ entry.bind("<FocusOut>", save_new_tag)
603
+ entry.bind("<Escape>", lambda e: entry.destroy())
604
+ entry.focus_set()
605
+
606
+ def edit_tag(tag_label):
607
+ """Edit the selected tag."""
608
+ def save_edit(event=None):
609
+ new_tag = entry.get().strip()
610
+ old_tag = tag_label.cget("text").strip()
611
+ if new_tag and new_tag != old_tag:
612
+ # Update the tag_dict with new tag
613
+ tag_dict[new_tag] = tag_dict.pop(old_tag)
614
+
615
+ # Update the captions in the respective files
616
+ for i, file_path in enumerate(selected_files):
617
+ caption_file = os.path.join(save_directory, f"{os.path.basename(file_path)}_tags.txt")
618
+ if os.path.exists(caption_file):
619
+ with open(caption_file, 'r', encoding='utf-8') as file:
620
+ caption_text = file.read()
621
+ new_caption_text = caption_text.replace(old_tag, new_tag)
622
+ with open(caption_file, 'w', encoding='utf-8') as file:
623
+ file.write(new_caption_text)
624
+
625
+ update_image_preview(content_canvas) # Update image preview
626
+ update_tag_display() # Update the display of tags
627
+
628
+ old_tag = tag_label.cget("text").strip()
629
+ entry = tk.Entry(tag_text_frame)
630
+ entry.insert(0, old_tag)
631
+ entry.grid(row=tag_label.grid_info()["row"], column=0, sticky="w", padx=2, pady=1)
632
+ entry.bind("<Return>", save_edit)
633
+ entry.bind("<FocusOut>", save_edit)
634
+ entry.bind("<Escape>", lambda e: entry.destroy())
635
+ entry.focus_set()
636
+
637
+ def delete_tag(tag_label):
638
+ """Delete the selected tag."""
639
+ global selected_tag
640
+ if tag_label:
641
+ tag_to_delete = tag_label.cget("text").strip()
642
+ if tag_to_delete in tag_dict:
643
+ del tag_dict[tag_to_delete]
644
+
645
+ # Update the captions in the respective files
646
+ for i, file_path in enumerate(selected_files):
647
+ caption_file = os.path.join(save_directory, f"{os.path.basename(file_path)}_tags.txt")
648
+ if os.path.exists(caption_file):
649
+ with open(caption_file, 'r', encoding='utf-8') as file:
650
+ caption_text = file.read()
651
+ new_caption_text = caption_text.replace(tag_to_delete, "")
652
+ with open(caption_file, 'w', encoding='utf-8') as file:
653
+ file.write(new_caption_text)
654
+
655
+ update_image_preview(content_canvas) # Update image preview
656
+ update_tag_display() # Update the display of tags
657
+
658
+ def delete_all_files_with_tag(tag_label):
659
+ """Delete all images and their corresponding text files containing the tag."""
660
+ tag_to_delete = tag_label.cget("text").strip()
661
+ if tag_to_delete in tag_dict:
662
+ del tag_dict[tag_to_delete]
663
+
664
+ # Delete the files containing the tag
665
+ files_to_delete = []
666
+ for i, file_path in enumerate(selected_files):
667
+ caption_file = os.path.join(save_directory, f"{os.path.basename(file_path)}_tags.txt")
668
+ if os.path.exists(caption_file):
669
+ with open(caption_file, 'r', encoding='utf-8') as file:
670
+ caption_text = file.read()
671
+ if tag_to_delete in caption_text:
672
+ files_to_delete.append((file_path, caption_file))
673
+ for image_file, caption_file in files_to_delete:
674
+ # Remove image file
675
+ try:
676
+ os.remove(image_file)
677
+ except Exception as e:
678
+ error_messages.append(f"Error deleting image {image_file}: {str(e)}")
679
+ # Remove caption file
680
+ try:
681
+ os.remove(caption_file)
682
+ except Exception as e:
683
+ error_messages.append(f"Error deleting caption {caption_file}: {str(e)}")
684
+
685
+ # Remove deleted files from selected_files list
686
+ selected_files[:] = [file for file in selected_files if file not in [img for img, cap in files_to_delete]]
687
+
688
+ # Clear tag_dict and update tag display
689
+ update_tag_dict() # Update tag dictionary
690
+ update_tag_display() # Update the display of tags
691
+ update_image_preview(content_canvas) # Update image preview
692
+
693
+ def sort_tags_alpha():
694
+ """Sort tags alphabetically."""
695
+ global tag_dict
696
+ sorted_tags = sorted(tag_dict.items())
697
+ tag_dict = dict(sorted_tags)
698
+ update_tag_display()
699
+
700
+ def sort_tags_count():
701
+ """Sort tags by count."""
702
+ global tag_dict
703
+ sorted_tags = sorted(tag_dict.items(), key=lambda item: item[1], reverse=True)
704
+ tag_dict = dict(sorted_tags)
705
+ update_tag_display()
706
+
707
+ # Create GUI components
708
+ back_button = tk.Button(root, text="<-", font=('Helvetica', 14), command=return_to_menu)
709
+ back_button.pack(anchor='nw', padx=10, pady=10)
710
+
711
+ title_label = tk.Label(root, text="Image Tag Generator", font=('Helvetica', 16))
712
+ title_label.pack(pady=10)
713
+
714
+ select_files_button = tk.Button(root, text="Select Files", command=select_files)
715
+ select_files_button.pack(pady=10)
716
+
717
+ num_files_label = tk.Label(root, textvariable=num_files_var)
718
+ num_files_label.pack(pady=5)
719
+
720
+ show_captions_button = tk.Button(root, text="Show Captions", command=open_caption_window)
721
+ show_captions_button.pack(pady=10)
722
+
723
+ general_threshold_label = tk.Label(root, text="General Threshold:")
724
+ general_threshold_label.pack(pady=5)
725
+ general_threshold_entry = tk.Entry(root, textvariable=general_threshold_var, justify='center', width=5, validate='key')
726
+ general_threshold_entry.pack(pady=5)
727
+
728
+ character_threshold_label = tk.Label(root, text="Character Threshold:")
729
+ character_threshold_label.pack(pady=5)
730
+ character_threshold_entry = tk.Entry(root, textvariable=character_threshold_var, justify='center', width=5, validate='key')
731
+ character_threshold_entry.pack(pady=5)
732
+
733
+ prepend_text_label = tk.Label(root, text="Prepend Text:")
734
+ prepend_text_label.pack(pady=5)
735
+ prepend_text_entry = tk.Entry(root, textvariable=prepend_text_var, justify='center', width=20)
736
+ prepend_text_entry.pack(pady=5)
737
+
738
+ append_text_label = tk.Label(root, text="Append Text:")
739
+ append_text_label.pack(pady=5)
740
+ append_text_entry = tk.Entry(root, textvariable=append_text_var, justify='center', width=20)
741
+ append_text_entry.pack(pady=5)
742
+
743
+ thread_count_label = tk.Label(root, text="Thread Count:")
744
+ thread_count_label.pack(pady=5)
745
+ thread_count_entry = tk.Entry(root, textvariable=thread_count_var, justify='center', width=5, validate='key')
746
+ thread_count_entry.pack(pady=5)
747
+
748
+ batch_size_label = tk.Label(root, text="Batch Size:")
749
+ batch_size_label.pack(pady=5)
750
+ batch_size_entry = tk.Entry(root, textvariable=batch_size_var, justify='center', width=5, validate='key')
751
+ batch_size_entry.pack(pady=5)
752
+
753
+ errors_button = tk.Button(root, textvariable=errors_var, command=lambda: show_errors(root))
754
+ errors_button.pack(pady=10)
755
+
756
+ start_button = tk.Button(root, text="Generate Captions", command=lambda: [process_files(), update_and_save_config()])
757
+ start_button.pack(pady=10)
758
+
759
+ stop_button = tk.Button(root, text="Stop", command=stop_processing_func)
760
+ stop_button.pack(pady=10)
761
+
762
+ progress_bar = ttk.Progressbar(root, variable=progress, maximum=100)
763
+ progress_bar.pack(pady=10, fill=tk.X)
764
+
765
+ status_label = tk.Label(root, textvariable=status_var, fg="green")
766
+ status_label.pack(pady=5)
767
+
768
+ # Add input validation
769
+ general_threshold_entry.config(validatecommand=(root.register(validate_threshold), '%P'))
770
+ character_threshold_entry.config(validatecommand=(root.register(validate_threshold), '%P'))
771
+ thread_count_entry.config(validatecommand=(root.register(validate_thread_count), '%P'))
772
+ batch_size_entry.config(validatecommand=(root.register(validate_batch_size), '%P'))
773
+
774
+ # Update config when the thresholds or thread count change
775
+ general_threshold_var.trace_add('write', lambda *args: update_and_save_config())
776
+ character_threshold_var.trace_add('write', lambda *args: update_and_save_config())
777
+ thread_count_var.trace_add('write', lambda *args: update_and_save_config())
778
+
779
+ center_window(root, width_extra=200)
780
+ root.protocol("WM_DELETE_WINDOW", on_closing)
781
+ root.mainloop()
782
+
783
+ if __name__ == "__main__":
784
+ open_image_to_tag()
main.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tkinter as tk
2
+ from tkinter import messagebox
3
+ import webbrowser
4
+ from image_converter import open_image_converter
5
+ from image_filter import open_image_filter
6
+ from rotate_flip import open_image_rotate_flip
7
+ from image_error_fix import open_image_error_fix
8
+ from image_to_tag import open_image_to_tag
9
+ from image_to_caption import open_image_to_caption # Import the function from image_to_caption.py
10
+ from photo_fantasy import open_photo_fantasy
11
+ from shuffle_image import open_image_shuffle # Import the function from shuffle_image.py
12
+
13
+ def open_main_menu():
14
+ root = tk.Tk()
15
+ root.title("IMageDucHaiten")
16
+
17
+ def center_window(window):
18
+ window.update_idletasks()
19
+ width = window.winfo_width()
20
+ height = window.winfo_height()
21
+ x = (window.winfo_screenwidth() // 2) - (width // 2)
22
+ y = (window.winfo_screenheight() // 2) - (height // 2)
23
+ window.geometry(f'{width}x{height}+{x}+{y}')
24
+
25
+ def open_converter_app():
26
+ root.destroy()
27
+ open_image_converter()
28
+
29
+ def open_image_filter_app():
30
+ root.destroy()
31
+ open_image_filter()
32
+
33
+ def open_rotate_flip_app():
34
+ root.destroy()
35
+ open_image_rotate_flip()
36
+
37
+ def open_image_error_fix_app():
38
+ root.destroy()
39
+ open_image_error_fix()
40
+
41
+ def open_image_to_tag_app():
42
+ root.destroy()
43
+ open_image_to_tag()
44
+
45
+ def open_image_to_caption_app():
46
+ root.destroy()
47
+ open_image_to_caption()
48
+
49
+ def open_photo_fantasy_app():
50
+ root.destroy()
51
+ open_photo_fantasy()
52
+
53
+ def open_shuffle_image_app():
54
+ root.destroy()
55
+ open_image_shuffle()
56
+
57
+ def open_future_app():
58
+ messagebox.showinfo("Information", "Other applications are still under development.")
59
+
60
+ def exit_app():
61
+ root.destroy()
62
+
63
+ def open_discord():
64
+ webbrowser.open("https://discord.gg/vKEW6jqa49")
65
+
66
+ def open_patreon():
67
+ webbrowser.open("https://www.patreon.com/duchaitenreal")
68
+
69
+ def open_paypal():
70
+ webbrowser.open("https://www.paypal.com/paypalme/duchaiten")
71
+
72
+ # Setup the interface
73
+ top_frame = tk.Frame(root)
74
+ top_frame.pack(fill='x', padx=10, pady=10)
75
+
76
+ discord_button = tk.Button(top_frame, text="Discord", font=('Helvetica', 12), command=open_discord)
77
+ discord_button.pack(side='left')
78
+
79
+ patreon_button = tk.Button(top_frame, text="Patreon", font=('Helvetica', 12), command=open_patreon)
80
+ patreon_button.pack(side='right')
81
+
82
+ paypal_button = tk.Button(top_frame, text="PayPal", font=('Helvetica', 12), command=open_paypal)
83
+ paypal_button.pack(side='right')
84
+
85
+ title_label = tk.Label(root, text="IMageDucHaiten", font=('Helvetica', 24))
86
+ title_label.pack(pady=20)
87
+
88
+ converter_button = tk.Button(root, text="Image Converter", font=('Helvetica', 16), command=open_converter_app)
89
+ converter_button.pack(pady=20)
90
+
91
+ image_filter_button = tk.Button(root, text="Image Filter", font=('Helvetica', 16), command=open_image_filter_app)
92
+ image_filter_button.pack(pady=20)
93
+
94
+ rotate_flip_button = tk.Button(root, text="Rotate & Flip", font=('Helvetica', 16), command=open_rotate_flip_app)
95
+ rotate_flip_button.pack(pady=20)
96
+
97
+ error_fix_button = tk.Button(root, text="Image Error Fix", font=('Helvetica', 16), command=open_image_error_fix_app)
98
+ error_fix_button.pack(pady=20)
99
+
100
+ caption_button = tk.Button(root, text="Image To Tag", font=('Helvetica', 16), command=open_image_to_tag_app)
101
+ caption_button.pack(pady=20)
102
+
103
+ image_to_caption_button = tk.Button(root, text="Image To Caption", font=('Helvetica', 16), command=open_image_to_caption_app)
104
+ image_to_caption_button.pack(pady=20)
105
+
106
+ photo_fantasy_button = tk.Button(root, text="Photo Fantasy", font=('Helvetica', 16), command=open_photo_fantasy_app)
107
+ photo_fantasy_button.pack(pady=20)
108
+
109
+ shuffle_image_button = tk.Button(root, text="Shuffle Image", font=('Helvetica', 16), command=open_shuffle_image_app)
110
+ shuffle_image_button.pack(pady=20)
111
+
112
+ future_app_button = tk.Button(root, text="Future App Template", font=('Helvetica', 16), command=open_future_app)
113
+ future_app_button.pack(pady=20)
114
+
115
+ exit_button = tk.Button(root, text="Exit", font=('Helvetica', 16), command=exit_app)
116
+ exit_button.pack(pady=20)
117
+
118
+ center_window(root)
119
+ root.geometry("550x950") # Set the window size larger
120
+ root.mainloop()
121
+
122
+ if __name__ == "__main__":
123
+ open_main_menu()
photo_fantasy.py ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tkinter as tk
2
+ from tkinter import filedialog, messagebox, ttk
3
+ import os
4
+ import threading
5
+ import queue
6
+ import shutil
7
+ import subprocess
8
+
9
+ # Global variables for controlling filtering and error handling
10
+ stop_event = threading.Event()
11
+ error_messages = []
12
+ error_window = None
13
+ selected_files = []
14
+ worker_thread = None
15
+
16
+ def open_photo_fantasy():
17
+ global error_messages, error_window, selected_files
18
+ global save_dir_var, status_var, num_files_var, errors_var, thread_count_var, progress
19
+ global q, worker_thread, root, stop_button, saved_files
20
+
21
+ # Create the Tkinter window
22
+ root = tk.Tk()
23
+ root.title("Photo Fantasy")
24
+
25
+ # Initialize Tkinter variables
26
+ save_dir_var = tk.StringVar()
27
+ status_var = tk.StringVar()
28
+ num_files_var = tk.StringVar()
29
+ errors_var = tk.StringVar(value="Errors: 0")
30
+ thread_count_var = tk.StringVar(value="4")
31
+ progress = tk.IntVar()
32
+ q = queue.Queue()
33
+
34
+ def center_window(window):
35
+ window.update_idletasks()
36
+ width = window.winfo_width() + 120 # Add 120 pixels to width
37
+ height = window.winfo_height()
38
+ x = (window.winfo_screenwidth() // 2) - (width // 2)
39
+ y = (window.winfo_screenheight() // 2) - (height // 2)
40
+ window.geometry(f'{width}x{height}+{x}+{y}')
41
+
42
+ def select_directory():
43
+ filepaths = filedialog.askopenfilenames(
44
+ title="Select Images",
45
+ filetypes=[("All Image files", "*.jpg;*.jpeg;*.png;*.gif;*.bmp;*.tiff")]
46
+ )
47
+ if filepaths:
48
+ selected_files.clear()
49
+ selected_files.extend(filepaths)
50
+ update_selected_files_label()
51
+
52
+ def choose_directory():
53
+ directory = filedialog.askdirectory()
54
+ if directory:
55
+ save_dir_var.set(directory)
56
+ save_dir_entry.config(state='normal')
57
+ save_dir_entry.delete(0, tk.END)
58
+ save_dir_entry.insert(0, directory)
59
+ save_dir_entry.config(state='readonly')
60
+
61
+ def save_file_with_unique_name(filepath, save_directory, saved_files):
62
+ """Save file with a unique name to avoid overwriting."""
63
+ if filepath in saved_files:
64
+ return # File already saved, do not save again
65
+
66
+ base_name, ext = os.path.splitext(os.path.basename(filepath))
67
+ save_path = os.path.join(save_directory, f"{base_name}{ext}")
68
+ counter = 1
69
+ while os.path.exists(save_path):
70
+ save_path = os.path.join(save_directory, f"{base_name} ({counter}){ext}")
71
+ counter += 1
72
+ try:
73
+ shutil.copy(filepath, save_path)
74
+ saved_files.add(filepath) # Mark this file as saved
75
+ except Exception as e:
76
+ error_messages.append(f"Error saving file {filepath}: {e}")
77
+ update_error_count()
78
+
79
+ def update_selected_files_label():
80
+ """Update the label showing the number of selected files."""
81
+ num_files_var.set(f"{len(selected_files)} files selected.")
82
+
83
+ def update_error_count():
84
+ """Update the error count displayed in the Errors button."""
85
+ errors_var.set(f"Errors: {len(error_messages)}")
86
+
87
+ def run_task(task_func):
88
+ """Run the given task function in a separate thread."""
89
+ global worker_thread
90
+ stop_event.clear()
91
+ disable_buttons()
92
+ worker_thread = threading.Thread(target=task_func)
93
+ worker_thread.start()
94
+ root.after(100, check_thread)
95
+
96
+ def check_thread():
97
+ """Check if the worker thread is still running."""
98
+ if worker_thread.is_alive():
99
+ root.after(100, check_thread)
100
+ else:
101
+ enable_buttons()
102
+ if stop_event.is_set():
103
+ status_var.set("Task stopped.")
104
+ else:
105
+ status_var.set("Task completed.")
106
+
107
+ def disable_buttons():
108
+ """Disable all buttons except the stop button."""
109
+ select_directory_button.config(state='disabled')
110
+ choose_dir_button.config(state='disabled')
111
+ auto_adjust_button.config(state='disabled')
112
+ enhance_vivid_button.config(state='disabled')
113
+ horror_theme_button.config(state='disabled')
114
+ cinematic_theme_button.config(state='disabled')
115
+ cyberpunk_theme_button.config(state='disabled')
116
+ fairytale_theme_button.config(state='disabled')
117
+ classic_vintage_button.config(state='disabled')
118
+ dark_fantasy_button.config(state='disabled')
119
+ stop_button.config(state='normal')
120
+
121
+ def enable_buttons():
122
+ """Enable all buttons."""
123
+ select_directory_button.config(state='normal')
124
+ choose_dir_button.config(state='normal')
125
+ auto_adjust_button.config(state='normal')
126
+ enhance_vivid_button.config(state='normal')
127
+ horror_theme_button.config(state='normal')
128
+ cinematic_theme_button.config(state='normal')
129
+ cyberpunk_theme_button.config(state='normal')
130
+ fairytale_theme_button.config(state='normal')
131
+ classic_vintage_button.config(state='normal')
132
+ dark_fantasy_button.config(state='normal')
133
+ stop_button.config(state='normal')
134
+
135
+ def process_images(process_func):
136
+ global saved_files
137
+ saved_files = set() # Initialize saved_files set
138
+
139
+ if not selected_files or not save_dir_var.get():
140
+ messagebox.showerror("Input Error", "Please select images and a save directory.")
141
+ enable_buttons()
142
+ return
143
+
144
+ save_directory = save_dir_var.get()
145
+ if not os.path.exists(save_directory):
146
+ os.makedirs(save_directory)
147
+
148
+ for file in selected_files:
149
+ if stop_event.is_set():
150
+ break
151
+
152
+ base_name, ext = os.path.splitext(os.path.basename(file))
153
+ save_path = os.path.join(save_directory, f"{base_name}{ext}")
154
+ counter = 1
155
+ while os.path.exists(save_path):
156
+ save_path = os.path.join(save_directory, f"{base_name} ({counter}){ext}")
157
+ counter += 1
158
+
159
+ try:
160
+ process_func(file, save_path)
161
+ saved_files.add(file) # Mark this file as saved
162
+ except subprocess.CalledProcessError as e:
163
+ error_messages.append(f"Error processing file {file}: {e}")
164
+ update_error_count()
165
+
166
+ messagebox.showinfo("Processing Complete", f"Processed {len(saved_files)} files.")
167
+
168
+ def auto_adjust_images():
169
+ process_images(lambda file, save_path: subprocess.run(['magick', 'convert', file,
170
+ '-enhance',
171
+ '-contrast-stretch', '0.1x0.1%',
172
+ '-sharpen', '0x1',
173
+ save_path], check=True))
174
+
175
+ def enhance_vivid_images():
176
+ process_images(lambda file, save_path: subprocess.run(['magick', 'convert', file,
177
+ '-enhance',
178
+ '-contrast-stretch', '0.1x0.1%',
179
+ '-sharpen', '0x1',
180
+ '-modulate', '105,120,100',
181
+ save_path], check=True))
182
+
183
+ def horror_theme_images():
184
+ process_images(lambda file, save_path: subprocess.run(['magick', 'convert', file,
185
+ '-modulate', '100,90,100',
186
+ '-level', '-5%,95%',
187
+ '-brightness-contrast', '1x1',
188
+ '-sigmoidal-contrast', '3x50%',
189
+ '-noise', '3',
190
+ '-sharpen', '0x1.5',
191
+ '(', '+clone', '-fill', 'black', '-colorize', '5%', ')',
192
+ '-compose', 'multiply', '-flatten',
193
+ save_path], check=True))
194
+
195
+ def cinematic_theme_images():
196
+ process_images(lambda file, save_path: subprocess.run(['magick', 'convert', file,
197
+ '-level', '-5%,95%',
198
+ '-modulate', '100,150,100',
199
+ '-colorize', '0,5,0',
200
+ '-brightness-contrast', '5x-0',
201
+ '-sigmoidal-contrast', '3x50%',
202
+ '-sharpen', '0x1.5',
203
+ '-noise', '0.1',
204
+ '(', '+clone', '-blur', '0x1', ')',
205
+ '-compose', 'blend', '-define', 'compose:args=10', '-composite',
206
+ '(', '+clone', '-fill', 'black', '-colorize', '10%', ')',
207
+ '-compose', 'multiply', '-flatten',
208
+ save_path], check=True))
209
+
210
+ def cyberpunk_theme_images():
211
+ process_images(lambda file, save_path: subprocess.run(['magick', 'convert', file,
212
+ '-modulate', '100,130,100',
213
+ '-level', '-5%,95%',
214
+ '-colorize', '10,0,20',
215
+ '-brightness-contrast', '1x1',
216
+ '-sigmoidal-contrast', '3x50%',
217
+ '-sharpen', '0x0.5',
218
+ '-noise', '0.5',
219
+ '(', '+clone', '-blur', '0x2', ')',
220
+ '-compose', 'blend', '-define', 'compose:args=20', '-composite',
221
+ '(', '+clone', '-fill', 'black', '-colorize', '10%', ')',
222
+ '-compose', 'multiply', '-flatten',
223
+ save_path], check=True))
224
+
225
+ def fairytale_theme_images():
226
+ process_images(lambda file, save_path: subprocess.run(['magick', 'convert', file,
227
+ '-modulate', '100,120,100',
228
+ '-blur', '0x1.2',
229
+ '-brightness-contrast', '2x-1',
230
+ '(', '+clone', '-alpha', 'extract', '-virtual-pixel', 'black',
231
+ '-blur', '0x15', '-shade', '120x45', ')',
232
+ '-compose', 'softlight', '-composite',
233
+ save_path], check=True))
234
+
235
+ def classic_vintage_images():
236
+ process_images(lambda file, save_path: subprocess.run(['magick', 'convert', file,
237
+ '-modulate', '110,80,100',
238
+ '-fill', '#704214', '-colorize', '10%',
239
+ '-attenuate', '0.3', '+noise', 'Multiplicative',
240
+ '-blur', '0x1.2',
241
+ '-level', '5%,90%',
242
+ '-unsharp', '0x5',
243
+ '-colorspace', 'sRGB',
244
+ '-brightness-contrast', '-5x15',
245
+ save_path], check=True))
246
+
247
+ def dark_fantasy_images():
248
+ process_images(lambda file, save_path: subprocess.run(['magick', 'convert', file,
249
+ '-modulate', '110,130,100',
250
+ '-blur', '0x1',
251
+ '-brightness-contrast', '5x-10',
252
+ '-attenuate', '0.1', '+noise', 'Multiplicative',
253
+ '-unsharp', '0x5',
254
+ '-level', '5%,95%',
255
+ '-modulate', '105,125,100',
256
+ '-brightness-contrast', '0x1',
257
+ '(', '+clone', '-fill', 'black', '-colorize', '10%', ')',
258
+ '-compose', 'multiply', '-flatten',
259
+ '-colorspace', 'sRGB',
260
+ save_path], check=True))
261
+
262
+ def stop_filtering_func():
263
+ stop_event.set() # Signal the worker thread to stop
264
+ status_var.set("Filtering stopped.")
265
+ # Do not disable the Stop button to keep it always enabled
266
+ # Re-enable all buttons
267
+ enable_buttons()
268
+ if worker_thread is not None:
269
+ worker_thread.join() # Wait for worker thread to finish
270
+
271
+ def return_to_menu():
272
+ stop_filtering_func()
273
+ root.destroy()
274
+ # Import main menu and open it
275
+ from main import open_main_menu
276
+ open_main_menu()
277
+
278
+ def on_closing():
279
+ stop_filtering_func()
280
+ return_to_menu()
281
+
282
+ def show_errors():
283
+ global error_window
284
+ if error_window is not None:
285
+ return
286
+
287
+ error_window = tk.Toplevel(root)
288
+ error_window.title("Error Details")
289
+ error_window.geometry("500x400")
290
+
291
+ error_text = tk.Text(error_window, wrap='word')
292
+ error_text.pack(expand=True, fill='both')
293
+
294
+ if error_messages:
295
+ for error in error_messages:
296
+ error_text.insert('end', error + '\n')
297
+ else:
298
+ error_text.insert('end', "No errors recorded.")
299
+
300
+ error_text.config(state='disabled')
301
+
302
+ def on_close_error_window():
303
+ global error_window
304
+ error_window.destroy()
305
+ error_window = None
306
+
307
+ error_window.protocol("WM_DELETE_WINDOW", on_close_error_window)
308
+
309
+ def validate_number(P):
310
+ if P.isdigit() or P == "":
311
+ return True
312
+ else:
313
+ messagebox.showerror("Input Error", "Please enter only numbers.")
314
+ return False
315
+
316
+ validate_command = root.register(validate_number)
317
+
318
+ # Create UI elements
319
+ back_button = tk.Button(root, text="<-", font=('Helvetica', 14), command=return_to_menu)
320
+ back_button.pack(anchor='nw', padx=10, pady=10)
321
+
322
+ title_label = tk.Label(root, text="Photo Fantasy", font=('Helvetica', 16))
323
+ title_label.pack(pady=10)
324
+
325
+ select_directory_button = tk.Button(root, text="Select Images", command=select_directory)
326
+ select_directory_button.pack(pady=5)
327
+
328
+ num_files_label = tk.Label(root, textvariable=num_files_var)
329
+ num_files_label.pack(pady=5)
330
+
331
+ choose_dir_button = tk.Button(root, text="Choose Save Directory", command=choose_directory)
332
+ choose_dir_button.pack(pady=5)
333
+
334
+ save_dir_entry = tk.Entry(root, textvariable=save_dir_var, state='readonly', justify='center')
335
+ save_dir_entry.pack(pady=5, fill=tk.X)
336
+
337
+ # Add Auto Adjust button
338
+ auto_adjust_button = tk.Button(root, text="Auto Adjust Images", command=lambda: run_task(auto_adjust_images))
339
+ auto_adjust_button.pack(pady=10)
340
+
341
+ # Add Enhance Vivid button
342
+ enhance_vivid_button = tk.Button(root, text="Enhance Vivid Images", command=lambda: run_task(enhance_vivid_images))
343
+ enhance_vivid_button.pack(pady=10)
344
+
345
+ # Add Horror Theme button
346
+ horror_theme_button = tk.Button(root, text="Horror Theme Images", command=lambda: run_task(horror_theme_images))
347
+ horror_theme_button.pack(pady=10)
348
+
349
+ # Add Cinematic Theme button
350
+ cinematic_theme_button = tk.Button(root, text="Cinematic Theme Images", command=lambda: run_task(cinematic_theme_images))
351
+ cinematic_theme_button.pack(pady=10)
352
+
353
+ # Add Cyberpunk Theme button
354
+ cyberpunk_theme_button = tk.Button(root, text="Cyberpunk Theme Images", command=lambda: run_task(cyberpunk_theme_images))
355
+ cyberpunk_theme_button.pack(pady=10)
356
+
357
+ # Add Fairytale Theme button
358
+ fairytale_theme_button = tk.Button(root, text="Fairytale Theme Images", command=lambda: run_task(fairytale_theme_images))
359
+ fairytale_theme_button.pack(pady=10)
360
+
361
+ # Add Classic Vintage button
362
+ classic_vintage_button = tk.Button(root, text="Classic Vintage Images", command=lambda: run_task(classic_vintage_images))
363
+ classic_vintage_button.pack(pady=10)
364
+
365
+ # Add Dark Fantasy button
366
+ dark_fantasy_button = tk.Button(root, text="Dark Fantasy Images", command=lambda: run_task(dark_fantasy_images))
367
+ dark_fantasy_button.pack(pady=10)
368
+
369
+ # Add label and entry for thread count
370
+ thread_count_label = tk.Label(root, text="Number of Threads:")
371
+ thread_count_label.pack(pady=5)
372
+
373
+ thread_count_entry = tk.Entry(root, textvariable=thread_count_var, validate="key", validatecommand=(validate_command, '%P'), justify='center', width=4)
374
+ thread_count_entry.pack(pady=5)
375
+
376
+ stop_button = tk.Button(root, text="Stop", command=stop_filtering_func) # Ensure stop button is a global variable
377
+ stop_button.pack(pady=5)
378
+
379
+ errors_button = tk.Button(root, textvariable=errors_var, command=show_errors)
380
+ errors_button.pack(pady=5)
381
+
382
+ progress_bar = ttk.Progressbar(root, variable=progress, maximum=100)
383
+ progress_bar.pack(pady=5, fill=tk.X)
384
+
385
+ status_label = tk.Label(root, textvariable=status_var, fg="green")
386
+ status_label.pack(pady=5)
387
+
388
+ center_window(root)
389
+ root.protocol("WM_DELETE_WINDOW", on_closing)
390
+ root.mainloop()
391
+
392
+ if __name__ == "__main__":
393
+ open_photo_fantasy()
requirements.txt ADDED
Binary file (2.45 kB). View file
 
rotate_flip.py ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tkinter as tk
2
+ from tkinter import filedialog, ttk, messagebox
3
+ from wand.image import Image as WandImage
4
+ import os
5
+ import threading
6
+ import queue
7
+
8
+ # Biến toàn cục để điều khiển việc dừng và lưu lỗi
9
+ stop_processing = False
10
+ error_messages = []
11
+ error_window = None
12
+ selected_files = []
13
+ save_directory = ""
14
+
15
+ def open_image_rotate_flip():
16
+ global stop_processing, error_messages, error_window, selected_files, save_dir_var, status_var, num_files_var, errors_var, thread_count_var, progress, q, rotate_left_angle_var, rotate_right_angle_var
17
+
18
+ # Tạo cửa sổ Tkinter
19
+ root = tk.Tk()
20
+ root.title("Image Rotate & Flip")
21
+
22
+ # Khởi tạo các biến Tkinter
23
+ save_dir_var = tk.StringVar()
24
+ status_var = tk.StringVar()
25
+ num_files_var = tk.StringVar()
26
+ errors_var = tk.StringVar(value="Errors: 0")
27
+ thread_count_var = tk.StringVar(value="4")
28
+ rotate_left_angle_var = tk.StringVar(value="90")
29
+ rotate_right_angle_var = tk.StringVar(value="90")
30
+ progress = tk.IntVar()
31
+ q = queue.Queue()
32
+
33
+ def center_window(window):
34
+ window.update_idletasks()
35
+ width = window.winfo_width() + 120
36
+ height = window.winfo_height()
37
+ x = (window.winfo_screenwidth() // 2) - (width // 2)
38
+ y = (window.winfo_screenheight() // 2) - (height // 2)
39
+ window.geometry(f'{width}x{height}+{x}+{y}')
40
+
41
+ def select_files():
42
+ global selected_files
43
+ filetypes = [
44
+ ("All Image files", "*.jpg;*.jpeg;*.png;*.gif;*.bmp;*.tiff;*.tif;*.svg;*.webp"),
45
+ ("JPEG files", "*.jpg;*.jpeg"),
46
+ ("PNG files", "*.png"),
47
+ ("GIF files", "*.gif"),
48
+ ("BMP files", "*.bmp"),
49
+ ("TIFF files", "*.tiff;*.tif"),
50
+ ("SVG files", "*.svg"),
51
+ ("WEBP files", "*.webp")
52
+ ]
53
+ filepaths = filedialog.askopenfilenames(title="Select Image Files", filetypes=filetypes)
54
+ if filepaths:
55
+ selected_files.clear()
56
+ selected_files.extend(filepaths)
57
+ num_files_var.set(f"{len(selected_files)} files selected.")
58
+
59
+ def choose_directory():
60
+ global save_directory
61
+ directory = filedialog.askdirectory()
62
+ if directory:
63
+ save_directory = directory
64
+ save_dir_var.set(directory)
65
+
66
+ def rotate_image(input_path, angle, output_path):
67
+ """Xoay ảnh sử dụng ImageMagick thông qua Wand."""
68
+ try:
69
+ with WandImage(filename=input_path) as img:
70
+ img.rotate(angle)
71
+ img.save(filename=output_path)
72
+ except Exception as e:
73
+ raise RuntimeError(f"Error rotating image: {e}")
74
+
75
+ def flip_image(input_path, direction, output_path):
76
+ """Lật ảnh sử dụng ImageMagick thông qua Wand."""
77
+ try:
78
+ with WandImage(filename=input_path) as img:
79
+ if direction == "horizontal":
80
+ img.flip()
81
+ elif direction == "vertical":
82
+ img.flop()
83
+ img.save(filename=output_path)
84
+ except Exception as e:
85
+ raise RuntimeError(f"Error flipping image: {e}")
86
+
87
+ def process_image(input_path, save_directory, operation, angle, q):
88
+ if stop_processing:
89
+ return
90
+
91
+ filename = os.path.basename(input_path)
92
+ try:
93
+ output_path = os.path.join(save_directory, filename)
94
+ if operation == "rotate_left":
95
+ rotate_image(input_path, -angle, output_path)
96
+ elif operation == "rotate_right":
97
+ rotate_image(input_path, angle, output_path)
98
+ elif operation == "flip_horizontal":
99
+ flip_image(input_path, "horizontal", output_path)
100
+ elif operation == "flip_vertical":
101
+ flip_image(input_path, "vertical", output_path)
102
+
103
+ q.put(input_path)
104
+ except Exception as e:
105
+ error_message = f"Error processing {filename}: {str(e)}"
106
+ q.put(error_message)
107
+ error_messages.append(error_message)
108
+
109
+ def worker(save_directory, operation, num_threads, angle):
110
+ try:
111
+ progress.set(0)
112
+ for i, input_path in enumerate(selected_files, 1):
113
+ if stop_processing:
114
+ break
115
+
116
+ thread = threading.Thread(target=process_image, args=(input_path, save_directory, operation, angle, q))
117
+ thread.start()
118
+ thread.join()
119
+
120
+ q.put(None)
121
+ except Exception as e:
122
+ if not stop_processing:
123
+ q.put(e)
124
+
125
+ def update_progress():
126
+ try:
127
+ completed = 0
128
+ while True:
129
+ item = q.get()
130
+ if item is None:
131
+ break
132
+ if isinstance(item, str):
133
+ if "Error" in item:
134
+ root.after(0, errors_var.set, f"Errors: {len(error_messages)}")
135
+ continue
136
+ completed += 1
137
+ progress.set(int((completed / len(selected_files)) * 100))
138
+ if not stop_processing:
139
+ root.after(0, status_var.set, f"Processed {completed} files")
140
+ root.after(0, root.update_idletasks)
141
+ if not stop_processing:
142
+ root.after(0, progress.set(100))
143
+ show_completion_message(completed)
144
+ except Exception as e:
145
+ if not stop_processing:
146
+ root.after(0, status_var.set, f"Error: {e}")
147
+
148
+ def show_completion_message(completed):
149
+ message = f"Processing complete. {completed} files processed."
150
+ if error_messages:
151
+ message += f" {len(error_messages)} errors occurred."
152
+ messagebox.showinfo("Process Complete", message)
153
+
154
+ def process_files(operation):
155
+ global stop_processing, error_messages
156
+ stop_processing = False
157
+ error_messages.clear()
158
+ errors_var.set("Errors: 0")
159
+ if not selected_files or not save_directory:
160
+ status_var.set("Please select images and save location.")
161
+ return
162
+
163
+ num_threads = int(thread_count_var.get() or 4)
164
+ if operation in ["rotate_left", "rotate_right"]:
165
+ angle = int(rotate_left_angle_var.get() or 0 if operation == "rotate_left" else rotate_right_angle_var.get() or 0)
166
+ if angle < 0 or angle > 360:
167
+ messagebox.showerror("Invalid Input", "Please enter a valid angle between 0 and 360.")
168
+ return
169
+ else:
170
+ angle = 0
171
+
172
+ threading.Thread(target=worker, args=(save_directory, operation, num_threads, angle)).start()
173
+ threading.Thread(target=update_progress).start()
174
+
175
+ def validate_number(P):
176
+ return P.isdigit() or P == ""
177
+
178
+ def validate_angle_input(var):
179
+ value = var.get()
180
+ if value != "":
181
+ try:
182
+ int_value = int(value)
183
+ if int_value < 0 or int_value > 360:
184
+ raise ValueError
185
+ except ValueError:
186
+ messagebox.showerror("Invalid Input", "Please enter a valid angle between 0 and 360.")
187
+ var.set("")
188
+
189
+ def validate_thread_count(var):
190
+ value = var.get()
191
+ if value != "":
192
+ try:
193
+ int_value = int(value)
194
+ if int_value <= 0:
195
+ raise ValueError
196
+ except ValueError:
197
+ messagebox.showerror("Invalid Input", "Please enter a valid number of threads.")
198
+ var.set("")
199
+
200
+ def stop_processing_func():
201
+ global stop_processing
202
+ stop_processing = True
203
+ status_var.set("Processing stopped.")
204
+
205
+ def return_to_menu():
206
+ stop_processing_func()
207
+ root.destroy()
208
+ import main
209
+ main.open_main_menu()
210
+
211
+ def on_closing():
212
+ return_to_menu()
213
+
214
+ def show_errors():
215
+ global error_window
216
+ if error_window is not None:
217
+ return
218
+
219
+ error_window = tk.Toplevel(root)
220
+ error_window.title("Error Details")
221
+ error_window.geometry("500x400")
222
+
223
+ error_text = tk.Text(error_window, wrap='word')
224
+ error_text.pack(expand=True, fill='both')
225
+
226
+ if error_messages:
227
+ for error in error_messages:
228
+ error_text.insert('end', error + '\n')
229
+ else:
230
+ error_text.insert('end', "No errors recorded.")
231
+
232
+ error_text.config(state='disabled')
233
+
234
+ def on_close_error_window():
235
+ global error_window
236
+ error_window.destroy()
237
+ error_window = None
238
+
239
+ error_window.protocol("WM_DELETE_WINDOW", on_close_error_window)
240
+
241
+ # Tạo các thành phần giao diện
242
+ validate_command = root.register(validate_number)
243
+
244
+ back_button = tk.Button(root, text="<-", font=('Helvetica', 14), command=return_to_menu)
245
+ back_button.pack(anchor='nw', padx=10, pady=10)
246
+
247
+ title_label = tk.Label(root, text="Image Rotate & Flip", font=('Helvetica', 16))
248
+ title_label.pack(pady=10)
249
+
250
+ select_files_button = tk.Button(root, text="Select Files", command=select_files)
251
+ select_files_button.pack(pady=5)
252
+
253
+ num_files_label = tk.Label(root, textvariable=num_files_var)
254
+ num_files_label.pack(pady=5)
255
+
256
+ choose_dir_button = tk.Button(root, text="Choose Save Directory", command=choose_directory)
257
+ choose_dir_button.pack(pady=10)
258
+
259
+ save_dir_entry = tk.Entry(root, textvariable=save_dir_var, state='readonly', justify='center')
260
+ save_dir_entry.pack(pady=5, fill=tk.X)
261
+
262
+ rotate_left_label = tk.Label(root, text="Enter the rotation angle (°):")
263
+ rotate_left_label.pack(pady=5)
264
+ rotate_left_entry = tk.Entry(root, textvariable=rotate_left_angle_var, justify='center', width=5, validate="key", validatecommand=(validate_command, '%P'))
265
+ rotate_left_entry.pack(pady=5)
266
+
267
+ rotate_left_button = tk.Button(root, text="Rotate Left", command=lambda: process_files("rotate_left"))
268
+ rotate_left_button.pack(pady=5)
269
+
270
+ rotate_right_label = tk.Label(root, text="Enter the rotation angle (°):")
271
+ rotate_right_label.pack(pady=5)
272
+ rotate_right_entry = tk.Entry(root, textvariable=rotate_right_angle_var, justify='center', width=5, validate="key", validatecommand=(validate_command, '%P'))
273
+ rotate_right_entry.pack(pady=5)
274
+
275
+ rotate_right_button = tk.Button(root, text="Rotate Right", command=lambda: process_files("rotate_right"))
276
+ rotate_right_button.pack(pady=5)
277
+
278
+ # Đường kẻ phân cách
279
+ separator = ttk.Separator(root, orient='horizontal')
280
+ separator.pack(fill='x', pady=10)
281
+
282
+ flip_horizontal_button = tk.Button(root, text="Flip Horizontal", command=lambda: process_files("flip_horizontal"))
283
+ flip_horizontal_button.pack(pady=5)
284
+
285
+ flip_vertical_button = tk.Button(root, text="Flip Vertical", command=lambda: process_files("flip_vertical"))
286
+ flip_vertical_button.pack(pady=5)
287
+
288
+ thread_count_label = tk.Label(root, text="Number of Threads:")
289
+ thread_count_label.pack(pady=5)
290
+
291
+ thread_count_entry = tk.Entry(root, textvariable=thread_count_var, width=5, justify='center', validate="key", validatecommand=(validate_command, '%P'))
292
+ thread_count_entry.pack(pady=5)
293
+
294
+ stop_button = tk.Button(root, text="Stop", command=stop_processing_func)
295
+ stop_button.pack(pady=5)
296
+
297
+ errors_button = tk.Button(root, textvariable=errors_var, command=show_errors)
298
+ errors_button.pack(pady=5)
299
+
300
+ progress_bar = ttk.Progressbar(root, variable=progress, maximum=100)
301
+ progress_bar.pack(pady=5, fill=tk.X)
302
+
303
+ status_label = tk.Label(root, textvariable=status_var, fg="green")
304
+ status_label.pack(pady=5)
305
+
306
+ # Ràng buộc xác thực đầu vào cho các ô nhập
307
+ rotate_left_angle_var.trace_add('write', lambda *args: validate_angle_input(rotate_left_angle_var))
308
+ rotate_right_angle_var.trace_add('write', lambda *args: validate_angle_input(rotate_right_angle_var))
309
+ thread_count_var.trace_add('write', lambda *args: validate_thread_count(thread_count_var))
310
+
311
+ center_window(root)
312
+ root.protocol("WM_DELETE_WINDOW", on_closing)
313
+ root.mainloop()
314
+
315
+ if __name__ == "__main__":
316
+ open_image_rotate_flip()
shuffle_image.py ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tkinter as tk
2
+ from tkinter import filedialog, ttk, messagebox
3
+ import os
4
+ import threading
5
+ import queue
6
+ import random
7
+ import shutil
8
+
9
+ # Biến toàn cục để điều khiển việc dừng và lưu lỗi
10
+ stop_processing = False
11
+ error_messages = []
12
+ error_window = None
13
+ selected_files = []
14
+ save_directory = ""
15
+ random_selected_files = []
16
+ shuffle_enabled = True # Biến để lưu trạng thái xáo trộn
17
+
18
+ def open_image_shuffle():
19
+ global stop_processing, error_messages, error_window, selected_files, save_dir_var, status_var, num_files_var, errors_var, thread_count_var, progress, q, random_selected_files, random_file_count_var, shuffle_enabled
20
+
21
+ # Tạo cửa sổ Tkinter
22
+ root = tk.Tk()
23
+ root.title("Image Shuffle")
24
+
25
+ # Khởi tạo các biến Tkinter
26
+ save_dir_var = tk.StringVar()
27
+ status_var = tk.StringVar()
28
+ num_files_var = tk.StringVar()
29
+ errors_var = tk.StringVar(value="Errors: 0")
30
+ thread_count_var = tk.StringVar(value="4")
31
+ random_file_count_var = tk.StringVar(value="0")
32
+ progress = tk.IntVar()
33
+ q = queue.Queue()
34
+
35
+ def center_window(window):
36
+ window.update_idletasks()
37
+ width = window.winfo_width() + 120
38
+ height = window.winfo_height()
39
+ x = (window.winfo_screenwidth() // 2) - (width // 2)
40
+ y = (window.winfo_screenheight() // 2) - (height // 2)
41
+ window.geometry(f'{width}x{height}+{x}+{y}')
42
+
43
+ def select_files():
44
+ global selected_files
45
+ filetypes = [
46
+ ("All Image files", "*.jpg;*.jpeg;*.png;*.gif;*.bmp;*.tiff;*.tif;*.svg;*.webp"),
47
+ ("JPEG files", "*.jpg;*.jpeg"),
48
+ ("PNG files", "*.png"),
49
+ ("GIF files", "*.gif"),
50
+ ("BMP files", "*.bmp"),
51
+ ("TIFF files", "*.tiff;*.tif"),
52
+ ("SVG files", "*.svg"),
53
+ ("WEBP files", "*.webp")
54
+ ]
55
+ filepaths = filedialog.askopenfilenames(title="Select Image Files", filetypes=filetypes)
56
+ if filepaths:
57
+ selected_files.clear()
58
+ selected_files.extend(filepaths)
59
+ num_files_var.set(f"{len(selected_files)} files selected.")
60
+
61
+ def choose_directory():
62
+ global save_directory
63
+ directory = filedialog.askdirectory()
64
+ if directory:
65
+ save_directory = directory
66
+ save_dir_var.set(directory)
67
+
68
+ def shuffle_and_save_images(num_random):
69
+ """Chọn ngẫu nhiên và lưu ảnh."""
70
+ try:
71
+ if num_random > len(selected_files):
72
+ messagebox.showerror("Error", "Not enough images to select.")
73
+ return
74
+
75
+ random_selected_files = random.sample(selected_files, num_random)
76
+ if shuffle_enabled:
77
+ random.shuffle(random_selected_files) # Xáo trộn danh sách file nếu được bật
78
+
79
+ for i, original_path in enumerate(random_selected_files):
80
+ filename = os.path.basename(original_path)
81
+ new_filename = f"shuffled_{i+1:03d}_{filename}" # Tạo tên file mới với tiền tố shuffled và số thứ tự
82
+ new_path = os.path.join(save_directory, new_filename)
83
+ shutil.copy(original_path, new_path)
84
+ q.put(i + 1) # Gửi tiến trình hoàn thành cho hàng đợi
85
+
86
+ q.put(None) # Thông báo hoàn thành
87
+ except Exception as e:
88
+ q.put(e) # Gửi lỗi vào hàng đợi
89
+
90
+ def select_and_save_sequentially(num_random):
91
+ """Chọn và lưu ảnh theo thứ tự."""
92
+ try:
93
+ if num_random > len(selected_files):
94
+ messagebox.showerror("Error", "Not enough images to select.")
95
+ return
96
+
97
+ step = len(selected_files) // num_random
98
+ sequential_selected_files = selected_files[::step]
99
+
100
+ for i, original_path in enumerate(sequential_selected_files[:num_random]):
101
+ filename = os.path.basename(original_path)
102
+ new_filename = f"sequential_{i+1:03d}_{filename}" # Tạo tên file mới với tiền tố sequential và số thứ tự
103
+ new_path = os.path.join(save_directory, new_filename)
104
+ shutil.copy(original_path, new_path)
105
+ q.put(i + 1) # Gửi tiến trình hoàn thành cho hàng đợi
106
+
107
+ q.put(None) # Thông báo hoàn thành
108
+ except Exception as e:
109
+ q.put(e) # Gửi lỗi vào hàng đợi
110
+
111
+ def worker(num_random):
112
+ try:
113
+ progress.set(0)
114
+ if shuffle_enabled:
115
+ shuffle_and_save_images(num_random)
116
+ else:
117
+ select_and_save_sequentially(num_random)
118
+ except Exception as e:
119
+ q.put(e) # Gửi lỗi vào hàng đợi
120
+
121
+ def update_progress():
122
+ try:
123
+ completed = 0
124
+ total_files = int(random_file_count_var.get())
125
+ while True:
126
+ item = q.get()
127
+ if item is None:
128
+ break
129
+ if isinstance(item, Exception):
130
+ raise item
131
+ completed = item
132
+ progress.set(int((completed / total_files) * 100))
133
+ if not stop_processing:
134
+ root.after(0, status_var.set, f"Processed {completed} of {total_files} files")
135
+ root.after(0, root.update_idletasks)
136
+ if not stop_processing:
137
+ root.after(0, progress.set(100))
138
+ show_completion_message(completed)
139
+ except Exception as e:
140
+ root.after(0, status_var.set, f"Error: {e}")
141
+
142
+ def show_completion_message(completed):
143
+ message = f"Processing complete. {completed} files processed."
144
+ if error_messages:
145
+ message += f" {len(error_messages)} errors occurred."
146
+ messagebox.showinfo("Process Complete", message)
147
+
148
+ def validate_and_process():
149
+ global stop_processing, error_messages
150
+ stop_processing = False
151
+ error_messages.clear()
152
+ errors_var.set("Errors: 0")
153
+ if not selected_files or not save_directory:
154
+ status_var.set("Please select images and save location.")
155
+ return
156
+
157
+ num_random = int(random_file_count_var.get())
158
+
159
+ threading.Thread(target=worker, args=(num_random,)).start()
160
+ threading.Thread(target=update_progress).start()
161
+
162
+ def validate_number(P):
163
+ return P.isdigit() or P == ""
164
+
165
+ def toggle_shuffle():
166
+ global shuffle_enabled
167
+ shuffle_enabled = shuffle_var.get() == 1
168
+ if shuffle_enabled:
169
+ shuffle_and_save_button.config(text="Shuffle")
170
+ random_file_count_label.config(text="Number of Random Files:")
171
+ else:
172
+ shuffle_and_save_button.config(text="Sequentially")
173
+ random_file_count_label.config(text="Number of Files to Select:")
174
+
175
+ def stop_processing_func():
176
+ global stop_processing
177
+ stop_processing = True
178
+ status_var.set("Processing stopped.")
179
+
180
+ def return_to_menu():
181
+ stop_processing_func()
182
+ root.destroy()
183
+ import main
184
+ main.open_main_menu()
185
+
186
+ def on_closing():
187
+ return_to_menu()
188
+
189
+ def show_errors():
190
+ global error_window
191
+ if error_window is not None:
192
+ return
193
+
194
+ error_window = tk.Toplevel(root)
195
+ error_window.title("Error Details")
196
+ error_window.geometry("500x400")
197
+
198
+ error_text = tk.Text(error_window, wrap='word')
199
+ error_text.pack(expand=True, fill='both')
200
+
201
+ if error_messages:
202
+ for error in error_messages:
203
+ error_text.insert('end', error + '\n')
204
+ else:
205
+ error_text.insert('end', "No errors recorded.")
206
+
207
+ error_text.config(state='disabled')
208
+
209
+ def on_close_error_window():
210
+ global error_window
211
+ error_window.destroy()
212
+ error_window = None
213
+
214
+ error_window.protocol("WM_DELETE_WINDOW", on_close_error_window)
215
+
216
+ # Tạo các thành phần giao diện
217
+ validate_command = root.register(validate_number)
218
+
219
+ back_button = tk.Button(root, text="<-", font=('Helvetica', 14), command=return_to_menu)
220
+ back_button.pack(anchor='nw', padx=10, pady=10)
221
+
222
+ title_label = tk.Label(root, text="Image Shuffle", font=('Helvetica', 16))
223
+ title_label.pack(pady=10)
224
+
225
+ select_files_button = tk.Button(root, text="Select Files", command=select_files)
226
+ select_files_button.pack(pady=5)
227
+
228
+ num_files_label = tk.Label(root, textvariable=num_files_var)
229
+ num_files_label.pack(pady=5)
230
+
231
+ choose_dir_button = tk.Button(root, text="Choose Save Directory", command=choose_directory)
232
+ choose_dir_button.pack(pady=10)
233
+
234
+ save_dir_entry = tk.Entry(root, textvariable=save_dir_var, state='readonly', justify='center')
235
+ save_dir_entry.pack(pady=5, fill=tk.X)
236
+
237
+ # Mô tả về các lựa chọn Shuffle và Sequential
238
+ description_label = tk.Label(root, text="Choose to shuffle or select sequentially the images before saving them:", font=('Helvetica', 10))
239
+ description_label.pack(pady=5)
240
+
241
+ # Khung cho hai lựa chọn Shuffle và Sequential
242
+ option_frame = tk.Frame(root)
243
+ option_frame.pack(pady=5)
244
+
245
+ shuffle_var = tk.IntVar(value=1) # Biến để lưu trạng thái xáo trộn
246
+
247
+ shuffle_radio_button = tk.Radiobutton(option_frame, text="Shuffle", variable=shuffle_var, value=1, command=toggle_shuffle)
248
+ shuffle_radio_button.pack(side='left', padx=5)
249
+
250
+ sequential_radio_button = tk.Radiobutton(option_frame, text="Sequential", variable=shuffle_var, value=0, command=toggle_shuffle)
251
+ sequential_radio_button.pack(side='left', padx=5)
252
+
253
+ random_file_count_label = tk.Label(root, text="Number of Random Files:")
254
+ random_file_count_label.pack(pady=5)
255
+
256
+ random_file_count_entry = tk.Entry(root, textvariable=random_file_count_var, width=10, justify='center', validate="key", validatecommand=(validate_command, '%P'))
257
+ random_file_count_entry.pack(pady=5)
258
+
259
+ shuffle_and_save_button = tk.Button(root, text="Shuffle", command=validate_and_process)
260
+ shuffle_and_save_button.pack(pady=10)
261
+
262
+ thread_count_label = tk.Label(root, text="Number of Threads:")
263
+ thread_count_label.pack(pady=5)
264
+
265
+ thread_count_entry = tk.Entry(root, textvariable=thread_count_var, width=5, justify='center', validate="key", validatecommand=(validate_command, '%P'))
266
+ thread_count_entry.pack(pady=5)
267
+
268
+ stop_button = tk.Button(root, text="Stop", command=stop_processing_func)
269
+ stop_button.pack(pady=5)
270
+
271
+ errors_button = tk.Button(root, textvariable=errors_var, command=show_errors)
272
+ errors_button.pack(pady=5)
273
+
274
+ progress_bar = ttk.Progressbar(root, variable=progress, maximum=100)
275
+ progress_bar.pack(pady=5, fill=tk.X)
276
+
277
+ status_label = tk.Label(root, textvariable=status_var, fg="green")
278
+ status_label.pack(pady=5)
279
+
280
+ center_window(root)
281
+ root.protocol("WM_DELETE_WINDOW", on_closing)
282
+ root.mainloop()
283
+
284
+ if __name__ == "__main__":
285
+ open_image_shuffle()