Upload 12 files
Browse files- captions.json +13 -0
- config.json +6 -0
- image_converter.py +426 -0
- image_error_fix.py +348 -0
- image_filter.py +515 -0
- image_to_caption.py +844 -0
- image_to_tag.py +784 -0
- main.py +123 -0
- photo_fantasy.py +393 -0
- requirements.txt +0 -0
- rotate_flip.py +316 -0
- 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()
|