DucHaiten commited on
Commit
9f86577
1 Parent(s): a80991b

Update image_filter.py

Browse files
Files changed (1) hide show
  1. image_filter.py +556 -515
image_filter.py CHANGED
@@ -1,515 +1,556 @@
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()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import imagehash
9
+ from PIL import Image
10
+
11
+ # Global variables for controlling filtering and error handling
12
+ stop_event = threading.Event()
13
+ error_messages = []
14
+ error_window = None
15
+ filtered_hashes = set()
16
+ selected_files = []
17
+ worker_thread = None
18
+
19
+ def open_image_filter():
20
+ global error_messages, error_window, filtered_hashes, selected_files
21
+ global save_dir_var, status_var, num_files_var, errors_var, thread_count_var, progress
22
+ global q, format_filter_var, filter_duplicate_var, min_size_var, max_size_var
23
+ global min_total_resolution_var, max_total_resolution_var, format_mode_var, format_filter_label
24
+ global delete_originals_var, worker_thread, root, stop_button, saved_files
25
+
26
+ # Create the Tkinter window
27
+ root = tk.Tk()
28
+ root.title("Image Filter")
29
+
30
+ # Initialize Tkinter variables
31
+ save_dir_var = tk.StringVar()
32
+ status_var = tk.StringVar()
33
+ num_files_var = tk.StringVar()
34
+ errors_var = tk.StringVar(value="Errors: 0")
35
+ thread_count_var = tk.StringVar(value="1")
36
+ progress = tk.IntVar()
37
+ q = queue.Queue()
38
+ format_filter_var = tk.StringVar()
39
+ filter_duplicate_var = tk.BooleanVar()
40
+ min_size_var = tk.IntVar()
41
+ max_size_var = tk.IntVar()
42
+ min_total_resolution_var = tk.IntVar()
43
+ max_total_resolution_var = tk.IntVar()
44
+ format_mode_var = tk.StringVar(value="exclude") # 'include' or 'exclude'
45
+
46
+ # Initialize variable for deleting original images
47
+ delete_originals_var = tk.BooleanVar()
48
+
49
+ def center_window(window):
50
+ window.update_idletasks()
51
+ width = window.winfo_width() + 120 # Add 120 pixels to width
52
+ height = window.winfo_height()
53
+ x = (window.winfo_screenwidth() // 2) - (width // 2)
54
+ y = (window.winfo_screenheight() // 2) - (height // 2)
55
+ window.geometry(f'{width}x{height}+{x}+{y}')
56
+
57
+ def select_directory():
58
+ filepaths = filedialog.askopenfilenames(
59
+ title="Select Images",
60
+ filetypes=[("All Image files", "*.jpg;*.jpeg;*.png;*.gif;*.bmp;*.tiff")]
61
+ )
62
+ if filepaths:
63
+ selected_files.clear()
64
+ selected_files.extend(filepaths)
65
+ update_selected_files_label()
66
+
67
+ def choose_directory():
68
+ directory = filedialog.askdirectory()
69
+ if directory:
70
+ save_dir_var.set(directory)
71
+ save_dir_entry.config(state='normal')
72
+ save_dir_entry.delete(0, tk.END)
73
+ save_dir_entry.insert(0, directory)
74
+ save_dir_entry.config(state='readonly')
75
+
76
+ def hash_image(file_path):
77
+ """Create a hash based on image pixels to identify similar images."""
78
+ try:
79
+ image = Image.open(file_path)
80
+ return str(imagehash.phash(image)) # Sử dụng perceptual hash
81
+ except Exception as e:
82
+ error_messages.append(f"Error hashing file {file_path}: {e}")
83
+ update_error_count()
84
+ return None
85
+
86
+ def filter_duplicate_images(filepaths):
87
+ unique_images = {}
88
+ filtered_files = []
89
+ for filepath in filepaths:
90
+ image_hash = hash_image(filepath)
91
+ if image_hash:
92
+ if image_hash in unique_images:
93
+ existing_file = unique_images[image_hash]
94
+
95
+ # So sánh và chọn ảnh có độ phân giải lớn hơn
96
+ try:
97
+ existing_image = Image.open(existing_file)
98
+ current_image = Image.open(filepath)
99
+ existing_resolution = existing_image.size[0] * existing_image.size[1]
100
+ current_resolution = current_image.size[0] * current_image.size[1]
101
+
102
+ # Giữ ảnh có độ phân giải lớn hơn
103
+ if current_resolution > existing_resolution:
104
+ unique_images[image_hash] = filepath
105
+ except Exception as e:
106
+ error_messages.append(f"Error reading image {filepath}: {e}")
107
+ update_error_count()
108
+ else:
109
+ unique_images[image_hash] = filepath
110
+ else:
111
+ filtered_files.append(filepath)
112
+
113
+ # Thêm ảnh duy nhất vào kết quả
114
+ filtered_files.extend(unique_images.values())
115
+ return filtered_files
116
+
117
+ def parse_formats(format_string):
118
+ return [fmt.strip().lower() for fmt in format_string.split(',') if fmt.strip()]
119
+
120
+ def filter_image_formats(filepaths, include_formats):
121
+ filtered_files = []
122
+ formats = parse_formats(format_filter_var.get())
123
+ if not formats:
124
+ return filepaths # No filtering if the format list is empty
125
+
126
+ for filepath in filepaths:
127
+ ext = os.path.splitext(filepath)[1][1:].lower() # Get the file extension
128
+ if (ext in formats) == include_formats:
129
+ filtered_files.append(filepath)
130
+ return filtered_files
131
+
132
+ def filter_image_size(filepaths, min_size, max_size):
133
+ filtered_files = []
134
+ for filepath in filepaths:
135
+ size = os.path.getsize(filepath)
136
+ if (min_size <= 0 or size >= min_size) and (max_size <= 0 or size <= max_size):
137
+ filtered_files.append(filepath)
138
+ return filtered_files
139
+
140
+ def filter_image_resolution(filepaths, min_total_resolution, max_total_resolution):
141
+ filtered_files = []
142
+ for filepath in filepaths:
143
+ try:
144
+ image = Image.open(filepath)
145
+ width, height = image.size
146
+ total_resolution = width + height
147
+ if (min_total_resolution <= 0 or total_resolution >= min_total_resolution) and \
148
+ (max_total_resolution <= 0 or total_resolution <= max_total_resolution):
149
+ filtered_files.append(filepath)
150
+ except Exception as e:
151
+ error_messages.append(f"Error reading image {filepath}: {e}")
152
+ update_error_count()
153
+ continue
154
+ return filtered_files
155
+
156
+ def save_file_with_unique_name(filepath, save_directory, saved_files):
157
+ """Save file with a unique name to avoid overwriting."""
158
+ if filepath in saved_files:
159
+ return # File already saved, do not save again
160
+
161
+ base_name, ext = os.path.splitext(os.path.basename(filepath))
162
+ save_path = os.path.join(save_directory, f"{base_name}{ext}")
163
+ counter = 1
164
+ while os.path.exists(save_path):
165
+ save_path = os.path.join(save_directory, f"{base_name} ({counter}){ext}")
166
+ counter += 1
167
+ try:
168
+ shutil.copy(filepath, save_path)
169
+ saved_files.add(filepath) # Mark this file as saved
170
+ except Exception as e:
171
+ error_messages.append(f"Error saving file {filepath}: {e}")
172
+ update_error_count()
173
+
174
+ def delete_original_images():
175
+ """Delete the original images if delete_originals_var is set."""
176
+ if delete_originals_var.get():
177
+ # Iterate through a copy of selected_files to avoid modifying the list during iteration
178
+ for filepath in selected_files[:]:
179
+ try:
180
+ os.remove(filepath)
181
+ selected_files.remove(filepath) # Remove from selected_files if deleted
182
+ except FileNotFoundError:
183
+ error_messages.append(f"File not found for deletion: {filepath}")
184
+ update_error_count()
185
+ except Exception as e:
186
+ error_messages.append(f"Error deleting file {filepath}: {e}")
187
+ update_error_count()
188
+ update_selected_files_label()
189
+
190
+ def update_selected_files_label():
191
+ """Update the label showing the number of selected files."""
192
+ num_files_var.set(f"{len(selected_files)} files selected.")
193
+
194
+ def update_error_count():
195
+ """Update the error count displayed in the Errors button."""
196
+ errors_var.set(f"Errors: {len(error_messages)}")
197
+
198
+ def check_all_files_filtered(filtered_files, filter_type):
199
+ """Check if all files have been filtered out and display a specific error message."""
200
+ if not filtered_files:
201
+ error_message = f"All images would be filtered out by the selected {filter_type} filter. Please adjust the filter settings."
202
+ messagebox.showerror("Filtering Error", error_message)
203
+ return True
204
+ return False
205
+
206
+ def filter_images_preview(filepaths):
207
+ """
208
+ Preview the number of images left after applying the filters.
209
+ Return the count of images left after filtering.
210
+ """
211
+ filtered_files = filepaths[:]
212
+
213
+ # Preview filtering by image format
214
+ include_formats = format_mode_var.get() == "include"
215
+ filtered_files = filter_image_formats(filtered_files, include_formats)
216
+
217
+ # Preview filtering by image size
218
+ filtered_files = filter_image_size(filtered_files, min_size_var.get(), max_size_var.get())
219
+
220
+ # Preview filtering by total resolution
221
+ filtered_files = filter_image_resolution(filtered_files, min_total_resolution_var.get(), max_total_resolution_var.get())
222
+
223
+ # Preview filtering duplicates if selected
224
+ if filter_duplicate_var.get():
225
+ filtered_files = filter_duplicate_images(filtered_files)
226
+
227
+ return len(filtered_files)
228
+
229
+ def filter_images(save_directory):
230
+ global saved_files
231
+ saved_files = set() # Initialize saved_files set
232
+ num_initial_files = len(selected_files)
233
+
234
+ # Initialize progress tracking
235
+ if num_initial_files == 0:
236
+ return [], 0, 0, 0
237
+
238
+ try:
239
+ filtered_files = selected_files[:]
240
+ steps = 4 # Số bước lọc (format, size, resolution, duplicates)
241
+ step_progress = 100 // steps
242
+ progress_step = 0
243
+
244
+ # Filter by image format
245
+ include_formats = format_mode_var.get() == "include"
246
+ filtered_files = filter_image_formats(filtered_files, include_formats)
247
+ progress_step += step_progress
248
+ progress.set(progress_step)
249
+ root.update_idletasks()
250
+ if check_all_files_filtered(filtered_files, "format"):
251
+ return [], num_initial_files, 0, 0
252
+
253
+ # Filter by image size
254
+ filtered_files = filter_image_size(filtered_files, min_size_var.get(), max_size_var.get())
255
+ progress_step += step_progress
256
+ progress.set(progress_step)
257
+ root.update_idletasks()
258
+ if check_all_files_filtered(filtered_files, "size"):
259
+ return [], num_initial_files, 0, 0
260
+
261
+ # Filter by total resolution
262
+ filtered_files = filter_image_resolution(filtered_files, min_total_resolution_var.get(), max_total_resolution_var.get())
263
+ progress_step += step_progress
264
+ progress.set(progress_step)
265
+ root.update_idletasks()
266
+ if check_all_files_filtered(filtered_files, "resolution"):
267
+ return [], num_initial_files, 0, 0
268
+
269
+ # Filter duplicates if selected
270
+ if filter_duplicate_var.get():
271
+ filtered_files = filter_duplicate_images(filtered_files)
272
+ progress_step += step_progress
273
+ progress.set(progress_step)
274
+ root.update_idletasks()
275
+ if check_all_files_filtered(filtered_files, "duplicate"):
276
+ return [], num_initial_files, 0, 0
277
+
278
+ # Calculate the number of filtered out images
279
+ num_filtered_files = len(filtered_files)
280
+ num_filtered_out_files = num_initial_files - num_filtered_files
281
+
282
+ if not os.path.exists(save_directory):
283
+ os.makedirs(save_directory)
284
+
285
+ return filtered_files, num_initial_files, num_filtered_files, num_filtered_out_files
286
+
287
+ except Exception as e:
288
+ error_messages.append(str(e))
289
+ update_error_count()
290
+ return [], num_initial_files, 0, 0
291
+
292
+
293
+ def worker(save_directory, num_threads, q):
294
+ try:
295
+ filtered_files, num_initial_files, num_filtered_files, num_filtered_out_files = filter_images(save_directory)
296
+ if not filtered_files: # Kiểm tra nếu không còn ảnh sau lọc
297
+ q.put(None)
298
+ return # Dừng nếu không còn ảnh
299
+
300
+ batch_size = 10 # Số lượng ảnh xử lý mỗi lần
301
+ total_batches = len(filtered_files) // batch_size + 1
302
+
303
+ for batch_index in range(total_batches):
304
+ if stop_event.is_set():
305
+ break
306
+
307
+ # Xử lý từng lô ảnh một
308
+ batch_files = filtered_files[batch_index * batch_size:(batch_index + 1) * batch_size]
309
+ for i, file in enumerate(batch_files):
310
+ if stop_event.is_set():
311
+ break
312
+ save_file_with_unique_name(file, save_directory, saved_files)
313
+ q.put((batch_index * batch_size + i + 1, num_initial_files)) # Thêm vào hàng đợi tiến trình
314
+
315
+ q.put(None) # Đánh dấu hoàn thành
316
+ except Exception as e:
317
+ if not stop_event.is_set():
318
+ error_messages.append(str(e))
319
+ update_error_count()
320
+ q.put(str(e))
321
+ finally:
322
+ stop_event.clear() # Xóa event stop cho lần chạy tiếp theo
323
+
324
+
325
+ def update_progress():
326
+ try:
327
+ item = q.get_nowait()
328
+ if item is None:
329
+ # Đã hoàn thành
330
+ progress.set(100)
331
+ show_completion_message(len(selected_files), len(saved_files), len(selected_files) - len(saved_files))
332
+ delete_original_images() # Delete original images after completion
333
+ # Re-enable all buttons after completion
334
+ filter_button.config(state='normal')
335
+ select_directory_button.config(state='normal')
336
+ choose_dir_button.config(state='normal')
337
+ delete_originals_checkbox.config(state='normal')
338
+ elif isinstance(item, tuple):
339
+ completed, num_initial_files = item
340
+ progress.set(int((completed / num_initial_files) * 100))
341
+ elif isinstance(item, str) and "Error" in item:
342
+ error_messages.append(item)
343
+ update_error_count()
344
+ except queue.Empty:
345
+ pass
346
+ finally:
347
+ # Kiểm tra hàng đợi mỗi 100ms
348
+ root.after(100, update_progress)
349
+
350
+
351
+ def show_completion_message(num_initial_files, num_filtered_files, num_filtered_out_files):
352
+ message = (
353
+ f"Filtering complete.\n"
354
+ f"Total files selected: {num_initial_files}\n"
355
+ f"Files processed and saved: {num_filtered_files}\n"
356
+ f"Files filtered out: {num_filtered_out_files}\n"
357
+ f"{len(error_messages)} errors occurred."
358
+ )
359
+ messagebox.showinfo("Filtering Complete", message)
360
+
361
+ def filter_files():
362
+ global error_messages, error_window, worker_thread
363
+ stop_event.clear() # Clear the stop event before starting a new task
364
+ error_messages.clear()
365
+ update_error_count()
366
+ save_directory = save_dir_var.get()
367
+ try:
368
+ num_threads = int(thread_count_var.get() or 4)
369
+ if num_threads <= 0:
370
+ raise ValueError("Number of threads must be greater than 0.")
371
+ except ValueError as e:
372
+ messagebox.showerror("Input Error", f"Invalid number of threads: {e}")
373
+ return
374
+
375
+ if not selected_files or not save_directory:
376
+ status_var.set("Please select images and save location.")
377
+ return
378
+
379
+ # Preview filtered results
380
+ remaining_images = filter_images_preview(selected_files)
381
+ if remaining_images == 0:
382
+ messagebox.showerror("Filtering Error", "No images will remain after applying the filters. Please adjust the filter settings.")
383
+ return
384
+
385
+ # Disable all buttons except Stop button
386
+ filter_button.config(state='disabled')
387
+ select_directory_button.config(state='disabled')
388
+ choose_dir_button.config(state='disabled')
389
+ delete_originals_checkbox.config(state='disabled')
390
+
391
+ worker_thread = threading.Thread(target=worker, args=(save_directory, num_threads, q))
392
+ worker_thread.start()
393
+ threading.Thread(target=update_progress).start()
394
+
395
+ def stop_filtering_func():
396
+ stop_event.set() # Signal the worker thread to stop
397
+ status_var.set("Filtering stopped.")
398
+ # Do not disable the Stop button to keep it always enabled
399
+ # Re-enable all buttons
400
+ filter_button.config(state='normal')
401
+ select_directory_button.config(state='normal')
402
+ choose_dir_button.config(state='normal')
403
+ delete_originals_checkbox.config(state='normal')
404
+ if worker_thread is not None:
405
+ worker_thread.join() # Wait for worker thread to finish
406
+
407
+ def return_to_menu():
408
+ stop_filtering_func()
409
+ root.destroy()
410
+ # Import main menu and open it
411
+ from main import open_main_menu
412
+ open_main_menu()
413
+
414
+ def on_closing():
415
+ stop_filtering_func()
416
+ return_to_menu()
417
+
418
+ def show_errors():
419
+ global error_window
420
+ if error_window is not None:
421
+ return
422
+
423
+ error_window = tk.Toplevel(root)
424
+ error_window.title("Error Details")
425
+ error_window.geometry("500x400")
426
+
427
+ error_text = tk.Text(error_window, wrap='word')
428
+ error_text.pack(expand=True, fill='both')
429
+
430
+ if error_messages:
431
+ for error in error_messages:
432
+ error_text.insert('end', error + '\n')
433
+ else:
434
+ error_text.insert('end', "No errors recorded.")
435
+
436
+ error_text.config(state='disabled')
437
+
438
+ def on_close_error_window():
439
+ global error_window
440
+ error_window.destroy()
441
+ error_window = None
442
+
443
+ error_window.protocol("WM_DELETE_WINDOW", on_close_error_window)
444
+
445
+ def toggle_format_mode():
446
+ if format_mode_var.get() == "include":
447
+ format_filter_label.config(text="Include Image Formats (comma-separated, e.g., png,jpg):")
448
+ else:
449
+ format_filter_label.config(text="Exclude Image Formats (comma-separated, e.g., png,jpg):")
450
+
451
+ def validate_number(P):
452
+ if P.isdigit() or P == "":
453
+ return True
454
+ else:
455
+ messagebox.showerror("Input Error", "Please enter only numbers.")
456
+ return False
457
+
458
+ validate_command = root.register(validate_number)
459
+
460
+ # Create UI elements
461
+ back_button = tk.Button(root, text="<-", font=('Helvetica', 14), command=return_to_menu)
462
+ back_button.pack(anchor='nw', padx=10, pady=10)
463
+
464
+ title_label = tk.Label(root, text="Image Filter", font=('Helvetica', 16))
465
+ title_label.pack(pady=10)
466
+
467
+ select_directory_button = tk.Button(root, text="Select Images", command=select_directory)
468
+ select_directory_button.pack(pady=5)
469
+
470
+ num_files_label = tk.Label(root, textvariable=num_files_var)
471
+ num_files_label.pack(pady=5)
472
+
473
+ choose_dir_button = tk.Button(root, text="Choose Save Directory", command=choose_directory)
474
+ choose_dir_button.pack(pady=5)
475
+
476
+ save_dir_entry = tk.Entry(root, textvariable=save_dir_var, state='readonly', justify='center')
477
+ save_dir_entry.pack(pady=5, fill=tk.X)
478
+
479
+ # Checkbox to delete original images
480
+ delete_originals_checkbox = tk.Checkbutton(root, text="Delete Original Images After Filtering", variable=delete_originals_var)
481
+ delete_originals_checkbox.pack(pady=5)
482
+
483
+ # Toggle image format filter mode
484
+ format_mode_frame = tk.Frame(root)
485
+ format_mode_frame.pack(pady=5)
486
+ format_mode_label = tk.Label(format_mode_frame, text="Toggle Format Mode (Include/Exclude):")
487
+ format_mode_label.pack(side="left")
488
+
489
+ # Radio buttons
490
+ include_radio = tk.Radiobutton(format_mode_frame, text="Include Formats", variable=format_mode_var, value="include", command=toggle_format_mode)
491
+ include_radio.pack(side="left", padx=5)
492
+ exclude_radio = tk.Radiobutton(format_mode_frame, text="Exclude Formats", variable=format_mode_var, value="exclude", command=toggle_format_mode)
493
+ exclude_radio.pack(side="left")
494
+
495
+ # Description for image format filter mode
496
+ format_filter_label = tk.Label(root, text="Exclude Image Formats (comma-separated, e.g., png,jpg):")
497
+ format_filter_label.pack(pady=5)
498
+
499
+ format_filter_entry = tk.Entry(root, textvariable=format_filter_var, justify='center')
500
+ format_filter_entry.pack(pady=5, fill=tk.X)
501
+
502
+ min_size_label = tk.Label(root, text="Min Size (bytes):")
503
+ min_size_label.pack(pady=5)
504
+
505
+ min_size_entry = tk.Entry(root, textvariable=min_size_var, validate="key", validatecommand=(validate_command, '%P'), justify='center', width=8)
506
+ min_size_entry.pack(pady=5)
507
+
508
+ max_size_label = tk.Label(root, text="Max Size (bytes):")
509
+ max_size_label.pack(pady=5)
510
+
511
+ max_size_entry = tk.Entry(root, textvariable=max_size_var, validate="key", validatecommand=(validate_command, '%P'), justify='center', width=8)
512
+ max_size_entry.pack(pady=5)
513
+
514
+ min_resolution_label = tk.Label(root, text="Min Total Resolution (sum of width and height):")
515
+ min_resolution_label.pack(pady=5)
516
+
517
+ min_resolution_entry = tk.Entry(root, textvariable=min_total_resolution_var, validate="key", validatecommand=(validate_command, '%P'), justify='center', width=8)
518
+ min_resolution_entry.pack(pady=5)
519
+
520
+ max_resolution_label = tk.Label(root, text="Max Total Resolution (sum of width and height):")
521
+ max_resolution_label.pack(pady=5)
522
+
523
+ max_resolution_entry = tk.Entry(root, textvariable=max_total_resolution_var, validate="key", validatecommand=(validate_command, '%P'), justify='center', width=8)
524
+ max_resolution_entry.pack(pady=5)
525
+
526
+ # Add label and entry for thread count
527
+ thread_count_label = tk.Label(root, text="Number of Threads:")
528
+ thread_count_label.pack(pady=5)
529
+
530
+ thread_count_entry = tk.Entry(root, textvariable=thread_count_var, validate="key", validatecommand=(validate_command, '%P'), justify='center', width=4)
531
+ thread_count_entry.pack(pady=5)
532
+
533
+ filter_duplicate_checkbox = tk.Checkbutton(root, text="Filter Duplicate Images", variable=filter_duplicate_var)
534
+ filter_duplicate_checkbox.pack(pady=5)
535
+
536
+ filter_button = tk.Button(root, text="Filter", command=filter_files)
537
+ filter_button.pack(pady=10)
538
+
539
+ stop_button = tk.Button(root, text="Stop", command=stop_filtering_func) # Ensure stop button is a global variable
540
+ stop_button.pack(pady=5)
541
+
542
+ errors_button = tk.Button(root, textvariable=errors_var, command=show_errors)
543
+ errors_button.pack(pady=5)
544
+
545
+ progress_bar = ttk.Progressbar(root, variable=progress, maximum=100)
546
+ progress_bar.pack(pady=5, fill=tk.X)
547
+
548
+ status_label = tk.Label(root, textvariable=status_var, fg="green")
549
+ status_label.pack(pady=5)
550
+
551
+ center_window(root)
552
+ root.protocol("WM_DELETE_WINDOW", on_closing)
553
+ root.mainloop()
554
+
555
+ if __name__ == "__main__":
556
+ open_image_filter()