import tkinter as tk
from tkinter import filedialog, ttk, messagebox
from PIL import Image, ImageTk, ImageEnhance, ImageOps, ImageFilter # Added ImageFilter
import os
import json
from typing import Dict, List, Tuple
import math
from collections import deque
import pytesseract
import re
class ImageRenamer:
def __init__(self, root):
self.root = root
self.root.title("מערכת שינוי שמות תמונות")
self.root.geometry("1024x768")
# מילון לשמירת השמות החדשים ותמונות
self.new_names: Dict[str, ttk.Entry] = {}
self.thumbnail_size = self.load_settings().get('thumbnail_size', 150)
self.files: List[str] = []
self.photo_references = []
self.dragged_widget = None
self.drag_source_index = None
# היסטוריית פעולות ל-Undo/Redo
self.undo_stack = deque(maxlen=50)
self.redo_stack = deque(maxlen=50)
self.create_gui()
self.create_preview_window()
def create_preview_window(self):
# חלון תצוגה מקדימה
self.preview_window = tk.Toplevel(self.root)
self.preview_window.title("תצוגה מקדימה")
self.preview_window.geometry("800x600")
self.preview_window.withdraw() # הסתרת החלון בהתחלה
self.preview_label = ttk.Label(self.preview_window)
self.preview_label.pack(expand=True, fill=tk.BOTH)
# כפתור סגירה
close_btn = ttk.Button(self.preview_window, text="סגור",
command=lambda: self.preview_window.withdraw())
close_btn.pack(pady=10)
def create_gui(self):
# מסגרת עליונה לכפתורים וכלי שליטה
control_frame = ttk.Frame(self.root)
control_frame.pack(fill=tk.X, padx=5, pady=5)
# כפתורים שמאליים
left_frame = ttk.Frame(control_frame)
left_frame.pack(side=tk.LEFT, fill=tk.X)
select_btn = ttk.Button(left_frame, text="בחר תמונות", command=self.select_files)
select_btn.pack(side=tk.LEFT, padx=5)
# שליטה בגודל התמונות הממוזערות
ttk.Label(left_frame, text="גודל תמונות:").pack(side=tk.LEFT, padx=5)
self.size_var = tk.StringVar(value=str(self.thumbnail_size))
size_entry = ttk.Entry(left_frame, textvariable=self.size_var, width=5)
size_entry.pack(side=tk.LEFT)
update_size_btn = ttk.Button(left_frame, text="עדכן גודל",
command=lambda: self.update_thumbnail_size(int(self.size_var.get())))
update_size_btn.pack(side=tk.LEFT, padx=5)
# מסגרת לשינוי שמות אוטומטי
auto_rename_frame = ttk.LabelFrame(control_frame, text="שינוי שמות אוטומטי")
auto_rename_frame.pack(side=tk.LEFT, padx=20)
ttk.Label(auto_rename_frame, text="תבנית:").pack(side=tk.LEFT, padx=5)
self.pattern_var = tk.StringVar(value="image_{:03d}")
self.pattern_entry = ttk.Entry(auto_rename_frame, textvariable=self.pattern_var, width=15)
self.pattern_entry.pack(side=tk.LEFT, padx=5)
ttk.Button(auto_rename_frame, text="החל",
command=self.apply_auto_rename).pack(side=tk.LEFT, padx=5)
# כפתור OCR לזיהוי מספר ת.ז.
ocr_id_btn = ttk.Button(auto_rename_frame, text="OCR ת.ז.",
command=self.apply_ocr_rename)
ocr_id_btn.pack(side=tk.LEFT, padx=5)
# כפתורי Undo/Redo
undo_frame = ttk.Frame(control_frame)
undo_frame.pack(side=tk.LEFT, padx=20)
self.undo_btn = ttk.Button(undo_frame, text="בטל", command=self.undo_action)
self.undo_btn.pack(side=tk.LEFT, padx=2)
self.undo_btn.state(['disabled'])
self.redo_btn = ttk.Button(undo_frame, text="בצע שוב", command=self.redo_action)
self.redo_btn.pack(side=tk.LEFT, padx=2)
self.redo_btn.state(['disabled'])
# כפתור שמירה
save_btn = ttk.Button(control_frame, text="שמור שינויים", command=self.save_changes)
save_btn.pack(side=tk.RIGHT, padx=5)
# מסגרת לתצוגת התמונות
self.canvas_frame = ttk.Frame(self.root)
self.canvas_frame.pack(fill=tk.BOTH, expand=True)
# Canvas עם scrollbar
self.canvas = tk.Canvas(self.canvas_frame)
scrollbar_y = ttk.Scrollbar(self.canvas_frame, orient="vertical", command=self.canvas.yview)
scrollbar_x = ttk.Scrollbar(self.canvas_frame, orient="horizontal", command=self.canvas.xview)
scrollbar_y.pack(side=tk.RIGHT, fill=tk.Y)
scrollbar_x.pack(side=tk.BOTTOM, fill=tk.X)
self.canvas.configure(yscrollcommand=scrollbar_y.set, xscrollcommand=scrollbar_x.set)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# מסגרת פנימית לתוכן
self.inner_frame = ttk.Frame(self.canvas)
self.canvas.create_window((0, 0), window=self.inner_frame, anchor="nw")
# הגדרת אירועי גלילה וגרירה
self.inner_frame.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")))
self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)
# חלון מידע
self.status_label = ttk.Label(self.root, text="")
self.status_label.pack(pady=5)
def load_settings(self):
try:
with open('image_renamer_settings.json', 'r') as f:
return json.load(f)
except:
return {}
def save_settings(self):
settings = {
'thumbnail_size': self.thumbnail_size,
'pattern': self.pattern_var.get()
}
try:
with open('image_renamer_settings.json', 'w') as f:
json.dump(settings, f)
except Exception as e:
print(f"Error saving settings: {e}")
def show_preview(self, file_path):
try:
# טעינת התמונה המקורית
img = Image.open(file_path)
# חישוב יחס גודל חדש לשמירה על פרופורציות
display_size = (780, 580) # גודל מקסימלי לתצוגה
img.thumbnail(display_size, Image.Resampling.LANCZOS)
# המרה ל-PhotoImage
photo = ImageTk.PhotoImage(img)
# עדכון התווית
self.preview_label.configure(image=photo)
self.preview_label.image = photo
# הצגת החלון
self.preview_window.deiconify()
self.preview_window.lift()
except Exception as e:
messagebox.showerror("שגיאה", f"שגיאה בטעינת התמונה: {str(e)}")
def apply_auto_rename(self):
if not self.files:
return
pattern = self.pattern_var.get()
if not pattern:
messagebox.showerror("שגיאה", "נא להזין תבנית שם")
return
# שמירת מצב נוכחי להיסטוריה
old_names = {path: entry.get() for path, entry in self.new_names.items()}
try:
# שינוי שמות לפי התבנית
for i, file_path in enumerate(self.files):
new_name = pattern.format(i + 1) # מתחיל מ-1
self.new_names[file_path].delete(0, tk.END)
self.new_names[file_path].insert(0, new_name)
# הוספת הפעולה להיסטוריה
self.add_to_history('rename', old_names)
except Exception as e:
messagebox.showerror("שגיאה", f"שגיאה בשינוי שמות: {str(e)}")
def add_to_history(self, action_type, old_state):
if action_type == 'remove':
self.undo_stack.append(('remove', old_state)) # old_state is a tuple of (files_list, new_names_dict)
else:
self.undo_stack.append((action_type, old_state))
self.redo_stack.clear()
self.update_undo_redo_buttons()
def undo_action(self):
if not self.undo_stack:
return
action_type, old_state = self.undo_stack.pop()
# שמירת המצב הנוכחי ל-redo
current_state = (self.files.copy(), {path: entry.get() for path, entry in self.new_names.items()})
self.redo_stack.append((action_type, current_state))
if action_type == 'remove':
self.files, self.new_names = old_state
self.display_images() # Refresh display after undo remove
elif action_type == 'reorder':
self.files = old_state
self.display_images()
elif action_type in ['rename', 'ocr_rename', 'save']:
for path, old_name in old_state.items():
if path in self.new_names:
self.new_names[path].delete(0, tk.END)
self.new_names[path].insert(0, old_name)
self.update_undo_redo_buttons()
def redo_action(self):
if not self.redo_stack:
return
action_type, old_state = self.redo_stack.pop()
# שמירת המצב הנוכחי ל-undo
current_state = (self.files.copy(), {path: entry.get() for path, entry in self.new_names.items()})
self.undo_stack.append((action_type, current_state))
if action_type == 'remove':
self.files, self.new_names = old_state
self.display_images() # Refresh display after redo remove
elif action_type == 'reorder':
self.files = old_state
self.display_images()
elif action_type in ['rename', 'ocr_rename', 'save']:
for path, new_name in old_state.items():
if path in self.new_names:
self.new_names[path].delete(0, tk.END)
self.new_names[path].insert(0, new_name)
self.update_undo_redo_buttons()
def update_undo_redo_buttons(self):
self.undo_btn.state(['!disabled'] if self.undo_stack else ['disabled'])
self.redo_btn.state(['!disabled'] if self.redo_stack else ['disabled'])
def setup_drag_and_drop(self, frame, file_path, index):
frame.bind("<Button-1>", lambda e: self.start_drag(e, frame, index))
frame.bind("<B1-Motion>", self.drag)
frame.bind("<ButtonRelease-1>", self.drop)
def start_drag(self, event, widget, index):
self.dragged_widget = widget
self.drag_source_index = index
def drag(self, event):
if not self.dragged_widget:
return
pass
def drop(self, event):
if not self.dragged_widget or self.drag_source_index is None:
return
x = event.x_root - self.root.winfo_rootx()
y = event.y_root - self.root.winfo_rooty()
target_index = self.find_drop_target(x, y)
if target_index is not None and target_index != self.drag_source_index:
old_order = self.files.copy()
file_to_move = self.files.pop(self.drag_source_index)
self.files.insert(target_index, file_to_move)
self.add_to_history('reorder', old_order)
self.display_images()
self.dragged_widget = None
self.drag_source_index = None
def find_drop_target(self, x, y):
for idx, frame in enumerate(self.inner_frame.winfo_children()):
bbox = frame.winfo_geometry()
x1, y1, width, height = map(int, bbox.replace('x', '+').split('+'))
if x1 <= x <= x1 + width and y1 <= y <= y1 + height:
return idx
return None
def _on_mousewheel(self, event):
self.canvas.yview_scroll(int(-1*(event.delta/120)), "units")
def update_thumbnail_size(self, new_size):
if new_size < 50:
new_size = 50
elif new_size > 500:
new_size = 500
self.thumbnail_size = new_size
self.size_var.set(str(new_size))
self.save_settings()
if self.files:
self.display_images()
def select_files(self):
new_files = filedialog.askopenfilenames(
title="בחר תמונות",
filetypes=[
("Image files", "*.png *.jpg *.jpeg *.gif *.bmp")
]
)
if new_files:
self.files = list(new_files)
self.status_label.config(text=f"נבחרו {len(self.files)} תמונות")
self.undo_stack.clear()
self.redo_stack.clear()
self.update_undo_redo_buttons()
self.display_images()
def display_images(self):
# ניקוי תצוגה קודמת
for widget in self.inner_frame.winfo_children():
widget.destroy()
self.photo_references.clear()
self.new_names.clear()
if not self.files:
return
canvas_width = self.canvas.winfo_width()
num_columns = max(1, canvas_width // (self.thumbnail_size + 20))
for idx, file_path in enumerate(self.files):
row = idx // num_columns
col = idx % num_columns
frame = ttk.Frame(self.inner_frame)
frame.grid(row=row, column=col, padx=10, pady=10)
try:
img = Image.open(file_path)
img.thumbnail((self.thumbnail_size, self.thumbnail_size))
photo = ImageTk.PhotoImage(img)
self.photo_references.append(photo)
img_button = ttk.Button(frame, image=photo,
command=lambda fp=file_path: self.show_preview(fp))
img_button.image = photo
img_button.pack()
filename = os.path.splitext(os.path.basename(file_path))[0]
entry = ttk.Entry(frame, width=min(30, self.thumbnail_size // 10))
entry.insert(0, filename)
entry.pack(pady=5)
file_size = os.path.getsize(file_path) / (1024 * 1024)
size_label = ttk.Label(frame, text=f"{file_size:.1f} MB")
size_label.pack()
self.new_names[file_path] = entry
self.setup_drag_and_drop(frame, file_path, idx)
# כפתור הסרה
remove_btn = ttk.Button(frame, text="X", width=1, command=lambda fp=file_path: self.remove_image(fp))
remove_btn.pack(side=tk.TOP, anchor=tk.NE) # הצבה בפינה ימנית עליונה
except Exception as e:
messagebox.showerror("שגיאה", f"שגיאה בטעינת התמונה {file_path}: {str(e)}")
def extract_id_number_from_image(self, image_path):
"""
מחלץ מספר תעודת זהות (9 ספרות) מתמונה באמצעות OCR, כולל שיפור תמונה לטשטוש.
"""
try:
img = Image.open(image_path)
# **עיבוד תמונה מקדים לשיפור OCR**
img_gray = img.convert('L') # המרה לגווני אפור
img_blur = img_gray.filter(ImageFilter.GaussianBlur(radius=1)) # טשטוש גאוסיאני קל - הוספנו טשטוש
img_contrast = ImageEnhance.Contrast(img_blur).enhance(2.0) # הגברת קונטרסט
img_sharp = ImageEnhance.Sharpness(img_contrast).enhance(2.0) # הגברת חדות
img_threshold = ImageOps.autocontrast(img_sharp) # סף אדפטיבי (אוטומטי)
# img_threshold = img_gray.point(lambda p: 0 if p < 150 else 255, '1') # סף בינארי - אפשר לנסות במקום autocontrast
# המרת התמונה המעובדת לטקסט באמצעות OCR
config_params = '--psm 6' # מצב פילוח עמוד 6 - בלוק טקסט בודד - הוספנו psm
# config_params = '--psm 6 -c tessedit_char_whitelist=0123456789' # אפשרות לנסות whitelist רק ספרות (מורכב יותר)
text = pytesseract.image_to_string(img_threshold, lang='heb+eng', config=config_params) # שימוש בתמונה המעובדת, הוספנו config
print(f"טקסט OCR גולמי עבור {image_path}: \n{text}") # הדפסת טקסט גולמי לדיבוג - חשוב לבדוק
# המשך זיהוי מספר ת.ז. עם הביטוי הרגולרי
id_numbers_spaced = re.findall(r'\b([\d\s]+)\b', text) # ביטוי רגולרי - אולי כללי מדי
if id_numbers_spaced:
for spaced_id in id_numbers_spaced:
id_number = "".join(filter(str.isdigit, spaced_id))
if len(id_number) == 9:
return id_number
return None
except Exception as e:
print(f"שגיאת OCR עבור {image_path}: {e}")
return None
def apply_ocr_rename(self):
if not self.files:
return
old_names = {path: entry.get() for path, entry in self.new_names.items()}
try:
for file_path in self.files:
id_number = self.extract_id_number_from_image(file_path)
if id_number:
self.new_names[file_path].delete(0, tk.END)
self.new_names[file_path].insert(0, id_number)
self.status_label.config(text=f"זוהה מספר ת.ז. {id_number} עבור {os.path.basename(file_path)}")
else:
self.status_label.config(text=f"לא זוהה מספר ת.ז. עבור {os.path.basename(file_path)}")
except Exception as e:
messagebox.showerror("שגיאה", f"שגיאה בביצוע OCR: {str(e)}")
self.add_to_history('ocr_rename', old_names)
def remove_image(self, file_path):
"""
מסיר תמונה מרשימת התמונות המוצגות.
"""
if file_path not in self.files:
return
# שמירת מצב נוכחי להיסטוריה
old_state = (self.files.copy(), self.new_names.copy())
self.add_to_history('remove', old_state)
# הסרת הקובץ מהרשימות
self.files.remove(file_path)
if file_path in self.new_names:
del self.new_names[file_path]
self.display_images() # רענון התצוגה
def save_changes(self):
old_names = {path: os.path.splitext(os.path.basename(path))[0]
for path in self.files}
success_count = 0
error_count = 0
for old_path in self.files:
new_name = self.new_names[old_path].get().strip()
if new_name:
directory = os.path.dirname(old_path)
extension = os.path.splitext(old_path)[1]
new_path = os.path.join(directory, new_name + extension)
try:
if old_path != new_path:
os.rename(old_path, new_path)
success_count += 1
except Exception as e:
error_count += 1
messagebox.showerror("שגיאה", f"שגיאה בשינוי שם הקובץ {old_path}: {str(e)}")
if success_count > 0:
self.add_to_history('save', old_names)
status = f"הושלם בהצלחה! {success_count} קבצים שונו"
if error_count:
status += f", {error_count} שגיאות"
messagebox.showinfo("סיום", status)
if success_count > 0:
self.files = []
self.new_names.clear()
self.display_images()
def on_closing(self):
self.save_settings()
self.root.quit()
if __name__ == "__main__":
pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe' # נתיב Tesseract - **ודא שנכון!**
root = tk.Tk()
app = ImageRenamer(root)
root.protocol("WM_DELETE_WINDOW", app.on_closing)
root.mainloop()