""" GUI интерфейс для стеганографического приложения """ import tkinter as tk from tkinter import ttk, filedialog, messagebox import threading import os import sys # Добавляем родительскую директорию для импорта модулей core sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from core.lsb import encode_lsb, decode_lsb from core.dct import encode_dct, decode_dct from core.utils import calculate_capacity, calculate_psnr, text_to_bits class StegoGUI: """Главное окно приложения""" def __init__(self, root): self.root = root self.root.title("Stego App - Стеганографическое приложение") self.root.geometry("850x750") self.root.resizable(True, True) # Переменные self.encode_image_path = tk.StringVar() self.encode_output_path = tk.StringVar() self.decode_image_path = tk.StringVar() self.method_encode = tk.StringVar(value="lsb") self.method_decode = tk.StringVar(value="lsb") # Переменные для PSNR self.psnr_image1_path = tk.StringVar() self.psnr_image2_path = tk.StringVar() self.setup_ui() def setup_ui(self): """Создание интерфейса""" # Вкладки notebook = ttk.Notebook(self.root) notebook.pack(fill='both', expand=True, padx=10, pady=10) # Вкладка "Кодирование" self.encode_frame = ttk.Frame(notebook) notebook.add(self.encode_frame, text="Скрыть сообщение") self.setup_encode_tab() # Вкладка "Декодирование" self.decode_frame = ttk.Frame(notebook) notebook.add(self.decode_frame, text="Извлечь сообщение") self.setup_decode_tab() # Вкладка "Сравнить PSNR" self.psnr_frame = ttk.Frame(notebook) notebook.add(self.psnr_frame, text="Сравнить PSNR") self.setup_psnr_tab() # Вкладка "О программе" about_frame = ttk.Frame(notebook) notebook.add(about_frame, text="О программе") self.setup_about_tab(about_frame) def setup_encode_tab(self): """Вкладка кодирования""" row = 0 # Метод ttk.Label(self.encode_frame, text="Метод стеганографии:").grid(row=row, column=0, sticky='w', padx=5, pady=5) ttk.Radiobutton(self.encode_frame, text="LSB (рекомендуется)", variable=self.method_encode, value="lsb", command=self.update_max_chars).grid(row=row, column=1, padx=5, sticky='w') ttk.Radiobutton(self.encode_frame, text="DCT (для гладких изображений)", variable=self.method_encode, value="dct", command=self.update_max_chars).grid(row=row, column=2, padx=5, sticky='w') row += 1 # Разделитель ttk.Separator(self.encode_frame, orient='horizontal').grid(row=row, column=0, columnspan=4, sticky='ew', padx=5, pady=5) row += 1 # Исходное изображение ttk.Label(self.encode_frame, text="Исходное изображение:").grid(row=row, column=0, sticky='w', padx=5, pady=5) self.encode_image_entry = ttk.Entry(self.encode_frame, textvariable=self.encode_image_path, width=55) self.encode_image_entry.grid(row=row, column=1, columnspan=2, padx=5, sticky='ew') ttk.Button(self.encode_frame, text="Обзор...", command=self.select_encode_image).grid(row=row, column=3, padx=5) row += 1 # Выходное изображение ttk.Label(self.encode_frame, text="Сохранить как (PNG):").grid(row=row, column=0, sticky='w', padx=5, pady=5) self.encode_output_entry = ttk.Entry(self.encode_frame, textvariable=self.encode_output_path, width=55) self.encode_output_entry.grid(row=row, column=1, columnspan=2, padx=5, sticky='ew') ttk.Button(self.encode_frame, text="Обзор...", command=self.select_encode_output).grid(row=row, column=3, padx=5) row += 1 # Разделитель ttk.Separator(self.encode_frame, orient='horizontal').grid(row=row, column=0, columnspan=4, sticky='ew', padx=5, pady=5) row += 1 # Счётчик символов self.char_counter_label = ttk.Label(self.encode_frame, text="", foreground="gray") self.char_counter_label.grid(row=row, column=1, columnspan=2, sticky='w', padx=5) row += 1 # Сообщение ttk.Label(self.encode_frame, text="Сообщение для сокрытия:").grid(row=row, column=0, sticky='nw', padx=5, pady=5) self.message_text = tk.Text(self.encode_frame, height=8, width=65, wrap=tk.WORD) self.message_text.grid(row=row, column=1, columnspan=3, padx=5, pady=5, sticky='ew') self.message_text.bind('', self.update_char_counter) row += 1 # Кнопка очистки сообщения ttk.Button(self.encode_frame, text="Очистить сообщение", command=self.clear_message).grid(row=row, column=1, sticky='w', padx=5, pady=5) row += 1 # Разделитель ttk.Separator(self.encode_frame, orient='horizontal').grid(row=row, column=0, columnspan=4, sticky='ew', padx=5, pady=5) row += 1 # Вместимость в байтах self.capacity_label = ttk.Label(self.encode_frame, text="", foreground="gray") self.capacity_label.grid(row=row, column=1, columnspan=2, sticky='w', padx=5) row += 1 # Прогресс self.encode_progress = ttk.Progressbar(self.encode_frame, mode='indeterminate', length=400) # Кнопка self.encode_btn = ttk.Button(self.encode_frame, text="Скрыть сообщение", command=self.do_encode) self.encode_btn.grid(row=row, column=1, columnspan=2, pady=15) row += 1 # Статус self.status_label = ttk.Label(self.encode_frame, text="", foreground="blue") self.status_label.grid(row=row, column=0, columnspan=4, pady=5) # Привязка событий self.encode_image_path.trace('w', lambda *args: (self.update_capacity(), self.update_max_chars())) def setup_decode_tab(self): """Вкладка декодирования""" row = 0 # Метод ttk.Label(self.decode_frame, text="Метод стеганографии:").grid(row=row, column=0, sticky='w', padx=5, pady=5) ttk.Radiobutton(self.decode_frame, text="LSB (рекомендуется)", variable=self.method_decode, value="lsb").grid(row=row, column=1, padx=5, sticky='w') ttk.Radiobutton(self.decode_frame, text="DCT (для гладких изображений)", variable=self.method_decode, value="dct").grid(row=row, column=2, padx=5, sticky='w') row += 1 # Разделитель ttk.Separator(self.decode_frame, orient='horizontal').grid(row=row, column=0, columnspan=4, sticky='ew', padx=5, pady=5) row += 1 # Изображение ttk.Label(self.decode_frame, text="Изображение со скрытым сообщением:").grid(row=row, column=0, sticky='w', padx=5, pady=5) ttk.Entry(self.decode_frame, textvariable=self.decode_image_path, width=55).grid(row=row, column=1, columnspan=2, padx=5, sticky='ew') ttk.Button(self.decode_frame, text="Обзор...", command=self.select_decode_image).grid(row=row, column=3, padx=5) row += 1 # Разделитель ttk.Separator(self.decode_frame, orient='horizontal').grid(row=row, column=0, columnspan=4, sticky='ew', padx=5, pady=5) row += 1 # Прогресс self.decode_progress = ttk.Progressbar(self.decode_frame, mode='indeterminate', length=400) # Кнопка self.decode_btn = ttk.Button(self.decode_frame, text="Извлечь сообщение", command=self.do_decode) self.decode_btn.grid(row=row, column=1, columnspan=2, pady=15) row += 1 # Результат ttk.Label(self.decode_frame, text="Извлеченное сообщение:").grid(row=row, column=0, sticky='nw', padx=5, pady=5) self.result_text = tk.Text(self.decode_frame, height=8, width=65, wrap=tk.WORD) self.result_text.grid(row=row, column=1, columnspan=3, padx=5, pady=5, sticky='ew') row += 1 # Кнопки button_frame = ttk.Frame(self.decode_frame) button_frame.grid(row=row, column=1, columnspan=2, pady=5) self.save_btn = ttk.Button(button_frame, text="Сохранить в файл", command=self.save_result, state='disabled') self.save_btn.pack(side='left', padx=5) ttk.Button(button_frame, text="Очистить результат", command=self.clear_result).pack(side='left', padx=5) row += 1 # Статус self.decode_status = ttk.Label(self.decode_frame, text="", foreground="blue") self.decode_status.grid(row=row, column=0, columnspan=4, pady=5) self.decoded_message = "" def setup_psnr_tab(self): """Вкладка сравнения PSNR""" row = 0 ttk.Label(self.psnr_frame, text="Сравнение двух изображений (PSNR)", font=("Arial", 12, "bold")).grid(row=row, column=0, columnspan=4, pady=10) row += 1 ttk.Separator(self.psnr_frame, orient='horizontal').grid(row=row, column=0, columnspan=4, sticky='ew', padx=5, pady=5) row += 1 # Первое изображение ttk.Label(self.psnr_frame, text="Оригинальное изображение:").grid(row=row, column=0, sticky='w', padx=5, pady=5) ttk.Entry(self.psnr_frame, textvariable=self.psnr_image1_path, width=55).grid(row=row, column=1, columnspan=2, padx=5, sticky='ew') ttk.Button(self.psnr_frame, text="Обзор...", command=self.select_psnr_image1).grid(row=row, column=3, padx=5) row += 1 # Второе изображение ttk.Label(self.psnr_frame, text="Изменённое изображение:").grid(row=row, column=0, sticky='w', padx=5, pady=5) ttk.Entry(self.psnr_frame, textvariable=self.psnr_image2_path, width=55).grid(row=row, column=1, columnspan=2, padx=5, sticky='ew') ttk.Button(self.psnr_frame, text="Обзор...", command=self.select_psnr_image2).grid(row=row, column=3, padx=5) row += 1 ttk.Separator(self.psnr_frame, orient='horizontal').grid(row=row, column=0, columnspan=4, sticky='ew', padx=5, pady=10) row += 1 # Кнопка расчёта self.psnr_btn = ttk.Button(self.psnr_frame, text="Рассчитать PSNR", command=self.calculate_psnr) self.psnr_btn.grid(row=row, column=1, columnspan=2, pady=10) row += 1 # Результат ttk.Label(self.psnr_frame, text="Результат:", font=("Arial", 10, "bold")).grid(row=row, column=0, sticky='w', padx=5, pady=5) row += 1 self.psnr_result_text = tk.Text(self.psnr_frame, height=8, width=65, wrap=tk.WORD) self.psnr_result_text.grid(row=row, column=0, columnspan=4, padx=5, pady=5, sticky='ew') self.psnr_result_text.config(state='disabled') def setup_about_tab(self, frame): """Вкладка 'О программе'""" info_frame = ttk.Frame(frame) info_frame.pack(pady=20) ttk.Label(info_frame, text="Stego App", font=("Arial", 16, "bold")).pack(pady=5) ttk.Label(info_frame, text="Стеганографическое приложение", font=("Arial", 10)).pack(pady=2) ttk.Label(info_frame, text="").pack(pady=5) ttk.Label(info_frame, text="Курсовая работа", font=("Arial", 10, "bold")).pack(pady=2) ttk.Label(info_frame, text="Петров Игорь ИУ10-24").pack() ttk.Label(info_frame, text="").pack(pady=5) ttk.Separator(frame, orient='horizontal').pack(fill='x', padx=20, pady=10) tech_frame = ttk.Frame(frame) tech_frame.pack(pady=10) ttk.Label(tech_frame, text="Методы стеганографии:", font=("Arial", 10, "bold")).pack(anchor='w') ttk.Label(tech_frame, text=" • LSB (Least Significant Bit) - сокрытие в младших битах пикселей").pack(anchor='w') ttk.Label(tech_frame, text=" Работает с любыми изображениями, высокая вместимость").pack(anchor='w', padx=20) ttk.Label(tech_frame, text="").pack() ttk.Label(tech_frame, text=" • DCT (Discrete Cosine Transform) - сокрытие в частотной области").pack(anchor='w') ttk.Label(tech_frame, text=" Рекомендуется для гладких/однотонных изображений").pack(anchor='w', padx=20) ttk.Label(tech_frame, text="").pack() ttk.Label(tech_frame, text="Поддерживаемые форматы:", font=("Arial", 10, "bold")).pack(anchor='w') ttk.Label(tech_frame, text=" • PNG, BMP, JPG, JPEG").pack(anchor='w') ttk.Label(tech_frame, text="").pack() ttk.Label(tech_frame, text="Метрика качества:", font=("Arial", 10, "bold")).pack(anchor='w') ttk.Label(tech_frame, text=" • PSNR (Peak Signal-to-Noise Ratio)").pack(anchor='w') ttk.Label(tech_frame, text=" > 40 dB — отличное качество").pack(anchor='w', padx=20) ttk.Label(tech_frame, text=" 30-40 dB — хорошее качество").pack(anchor='w', padx=20) ttk.Label(tech_frame, text=" 20-30 dB — заметные искажения").pack(anchor='w', padx=20) ttk.Label(tech_frame, text=" < 20 dB — сильные искажения").pack(anchor='w', padx=20) # ==================== Функции для вкладки "Кодирование" ==================== def select_encode_image(self): """Выбор изображения для кодирования""" filename = filedialog.askopenfilename( title="Выберите исходное изображение", filetypes=[("Изображения", "*.png *.bmp *.jpg *.jpeg"), ("Все файлы", "*.*")] ) if filename: self.encode_image_path.set(filename) def select_encode_output(self): """Выбор выходного файла""" filename = filedialog.asksaveasfilename( title="Сохранить результат как", defaultextension=".png", filetypes=[("PNG", "*.png"), ("BMP", "*.bmp"), ("Все файлы", "*.*")] ) if filename: self.encode_output_path.set(filename) def clear_message(self): """Очистка поля сообщения""" self.message_text.delete("1.0", tk.END) self.update_char_counter() def update_capacity(self): """Обновление информации о вместимости""" path = self.encode_image_path.get() if path and os.path.exists(path): try: capacity = calculate_capacity(path) kb = capacity / 1024 self.capacity_label.config(text=f"📊 Максимальная вместимость: {capacity} байт (~{kb:.1f} КБ)") except Exception as e: self.capacity_label.config(text=f"⚠️ Ошибка: {e}") else: self.capacity_label.config(text="") def calculate_max_chars(self): """ Рассчитывает максимальное количество символов для текущего изображения и метода Возвращает кортеж (max_bytes, max_chars) """ image_path = self.encode_image_path.get() method = self.method_encode.get() if not image_path or not os.path.exists(image_path): return 0, 0 try: from PIL import Image if method == 'lsb': # LSB: 3 бита на пиксель, переводим в байты capacity_bytes = calculate_capacity(image_path) # Вычитаем 4 байта на длину сообщения max_bytes = max(0, capacity_bytes - 4) # Примерное количество символов (UTF-8, русские символы ~2 байта) max_chars = max_bytes // 2 # Осторожная оценка return max_bytes, max_chars else: # dct img = Image.open(image_path) w, h = img.size if w % 8 != 0 or h % 8 != 0: return 0, 0 # DCT: 39 бит на блок RGB, 3 канала blocks_per_row = w // 8 blocks_per_col = h // 8 total_blocks = blocks_per_row * blocks_per_col max_bits = total_blocks * 39 max_bytes = max_bits // 8 # Вычитаем 4 байта на длину сообщения max_bytes = max(0, max_bytes - 4) max_chars = max_bytes // 2 return max_bytes, max_chars except Exception as e: print(f"Ошибка расчёта: {e}") return 0, 0 def update_max_chars(self): """Обновляет отображение максимального количества символов""" max_bytes, max_chars = self.calculate_max_chars() if max_chars > 0: self.char_counter_label.config( text=f"📝 Максимум: ~{max_chars} символов (~{max_bytes} байт)", foreground="green" ) else: self.char_counter_label.config( text="⚠️ Выберите изображение (для DCT размер должен быть кратен 8)", foreground="orange" ) self.update_char_counter() return max_chars def update_char_counter(self, event=None): """Обновляет счётчик введённых символов""" message = self.message_text.get("1.0", tk.END).strip() current_bytes = len(message.encode('utf-8')) current_chars = len(message) max_bytes, max_chars = self.calculate_max_chars() if max_chars > 0: if current_bytes > max_bytes: self.char_counter_label.config( text=f"⚠️ ПРЕВЫШЕНИЕ: {current_chars} символов ({current_bytes} байт) > {max_bytes} байт", foreground="red" ) else: percent = (current_bytes / max_bytes) * 100 self.char_counter_label.config( text=f"📝 Введено: {current_chars} символов ({current_bytes} байт) / {max_bytes} байт ({percent:.0f}%)", foreground="green" if percent < 80 else "orange" ) def check_image_for_dct(self, image_path): """Проверка изображения для DCT метода""" try: from PIL import Image, ImageStat img = Image.open(image_path) w, h = img.size if w % 8 != 0 or h % 8 != 0: return False, f"Размеры изображения ({w}x{h}) должны быть кратны 8 для DCT метода" stat = ImageStat.Stat(img) if max(stat.stddev) > 50: return False, "Изображение содержит много деталей/цветов.\nDCT рекомендуется только для гладких/однотонных изображений.\nИспользуйте LSB метод." return True, "" except Exception as e: return False, str(e) def do_encode(self): """Выполнение кодирования""" image = self.encode_image_path.get() output = self.encode_output_path.get() message = self.message_text.get("1.0", tk.END).strip() method = self.method_encode.get() if not image: messagebox.showerror("Ошибка", "Выберите исходное изображение") return if not output: messagebox.showerror("Ошибка", "Укажите путь для сохранения результата") return if not message: messagebox.showerror("Ошибка", "Введите сообщение для сокрытия") return # Проверка превышения лимита max_bytes, _ = self.calculate_max_chars() msg_bytes = len(message.encode('utf-8')) if msg_bytes > max_bytes: response = messagebox.askyesno( "Превышение лимита", f"Сообщение ({msg_bytes} байт) превышает максимальную вместимость ({max_bytes} байт).\n" f"Часть сообщения будет обрезана.\n\n" f"Продолжить?" ) if not response: return if method == 'dct': is_ok, error_msg = self.check_image_for_dct(image) if not is_ok: messagebox.showerror("Ошибка", error_msg) return self.encode_btn.config(state='disabled') self.encode_progress.grid(row=15, column=1, columnspan=2, sticky='ew', padx=5, pady=5) self.encode_progress.start() self.status_label.config(text="⏳ Кодирование... Пожалуйста, подождите", foreground="blue") thread = threading.Thread(target=self._do_encode, args=(image, output, message, method)) thread.daemon = True thread.start() def _do_encode(self, image, output, message, method): """Фоновое кодирование""" try: if method == 'lsb': success = encode_lsb(image, message, output) else: success = encode_dct(image, message, output) self.root.after(0, self._encode_done, success, None, image, output) except Exception as e: self.root.after(0, self._encode_done, False, str(e), image, output) def _encode_done(self, success, error=None, original=None, modified=None): """Завершение кодирования""" self.encode_progress.stop() self.encode_progress.grid_forget() self.encode_btn.config(state='normal') if success: self.status_label.config(text="✅ Кодирование успешно завершено!", foreground="green") if original and modified: result = messagebox.askyesno( "Успех", f"Сообщение успешно скрыто в изображении!\n\n" f"Сохранено: {modified}\n\n" "Рассчитать PSNR (качество изображения)?" ) if result: self.calculate_psnr_pair(original, modified) else: error_msg = error or "Неизвестная ошибка" self.status_label.config(text=f"❌ Ошибка кодирования", foreground="red") messagebox.showerror("Ошибка", f"Не удалось скрыть сообщение:\n{error_msg}") # ==================== Функции для вкладки "Декодирование" ==================== def select_decode_image(self): """Выбор изображения для декодирования""" filename = filedialog.askopenfilename( title="Выберите изображение со скрытым сообщением", filetypes=[("Изображения", "*.png *.bmp *.jpg *.jpeg"), ("Все файлы", "*.*")] ) if filename: self.decode_image_path.set(filename) def clear_result(self): """Очистка поля результата""" self.result_text.delete("1.0", tk.END) self.decoded_message = "" self.save_btn.config(state='disabled') def do_decode(self): """Выполнение декодирования""" image = self.decode_image_path.get() method = self.method_decode.get() if not image: messagebox.showerror("Ошибка", "Выберите изображение") return self.decode_btn.config(state='disabled') self.decode_progress.grid(row=3, column=1, columnspan=2, sticky='ew', padx=5, pady=5) self.decode_progress.start() self.decode_status.config(text="⏳ Декодирование... Пожалуйста, подождите", foreground="blue") thread = threading.Thread(target=self._do_decode, args=(image, method)) thread.daemon = True thread.start() def _do_decode(self, image, method): """Фоновое декодирование""" try: if method == 'lsb': message = decode_lsb(image) else: message = decode_dct(image) self.root.after(0, self._decode_done, message, None) except Exception as e: self.root.after(0, self._decode_done, None, str(e)) def _decode_done(self, message, error=None): """Завершение декодирования""" self.decode_progress.stop() self.decode_progress.grid_forget() self.decode_btn.config(state='normal') if message and not error and message != "": self.decoded_message = message self.result_text.delete("1.0", tk.END) self.result_text.insert("1.0", message) self.save_btn.config(state='normal') self.decode_status.config(text="✅ Декодирование успешно завершено!", foreground="green") messagebox.showinfo("Успех", f"✅ Сообщение успешно извлечено!\n\nДлина: {len(message)} символов") else: error_msg = error or "Сообщение не найдено или повреждено" self.decode_status.config(text=f"❌ Ошибка декодирования", foreground="red") self.save_btn.config(state='disabled') messagebox.showerror("Ошибка", f"Не удалось извлечь сообщение:\n{error_msg}") def save_result(self): """Сохранение извлеченного сообщения в файл""" if not self.decoded_message: return filename = filedialog.asksaveasfilename( title="Сохранить сообщение", defaultextension=".txt", filetypes=[("Текстовые файлы", "*.txt"), ("Все файлы", "*.*")] ) if filename: try: with open(filename, 'w', encoding='utf-8') as f: f.write(self.decoded_message) messagebox.showinfo("Успех", f"✅ Сообщение сохранено в:\n{filename}") except Exception as e: messagebox.showerror("Ошибка", f"Не удалось сохранить файл:\n{e}") # ==================== Функции для вкладки "PSNR" ==================== def select_psnr_image1(self): """Выбор первого изображения для PSNR""" filename = filedialog.askopenfilename( title="Выберите оригинальное изображение", filetypes=[("Изображения", "*.png *.bmp *.jpg *.jpeg"), ("Все файлы", "*.*")] ) if filename: self.psnr_image1_path.set(filename) def select_psnr_image2(self): """Выбор второго изображения для PSNR""" filename = filedialog.askopenfilename( title="Выберите изменённое изображение", filetypes=[("Изображения", "*.png *.bmp *.jpg *.jpeg"), ("Все файлы", "*.*")] ) if filename: self.psnr_image2_path.set(filename) def calculate_psnr(self): """Расчёт PSNR между двумя выбранными изображениями""" img1 = self.psnr_image1_path.get() img2 = self.psnr_image2_path.get() if not img1 or not img2: messagebox.showerror("Ошибка", "Выберите оба изображения") return self.psnr_btn.config(state='disabled') self.psnr_result_text.config(state='normal') self.psnr_result_text.delete("1.0", tk.END) self.psnr_result_text.insert("1.0", "⏳ Расчёт PSNR... Пожалуйста, подождите") self.psnr_result_text.config(state='disabled') thread = threading.Thread(target=self._calculate_psnr, args=(img1, img2)) thread.daemon = True thread.start() def _calculate_psnr(self, img1_path, img2_path): """Фоновый расчёт PSNR""" try: psnr = calculate_psnr(img1_path, img2_path) if psnr == float('inf'): quality = "Идеальное (изображения идентичны)" color = "green" elif psnr > 40: quality = "Отличное (изменения практически незаметны)" color = "green" elif psnr > 30: quality = "Хорошее" color = "blue" elif psnr > 20: quality = "Заметные искажения" color = "orange" else: quality = "Сильные искажения" color = "red" result = f"PSNR: {psnr:.2f} dB\n\n" result += f"Оценка качества: {quality}\n\n" result += "Шкала PSNR:\n" result += " > 40 dB — отличное качество\n" result += " 30-40 dB — хорошее качество\n" result += " 20-30 dB — заметные искажения\n" result += " < 20 dB — сильные искажения\n\n" result += f"Оригинал: {img1_path}\n" result += f"Изменённое: {img2_path}" self.root.after(0, self._psnr_done, result, True, color) except Exception as e: self.root.after(0, self._psnr_done, f"Ошибка при расчёте PSNR:\n{str(e)}", False, "red") def _psnr_done(self, result, success, color="black"): """Завершение расчёта PSNR""" self.psnr_btn.config(state='normal') self.psnr_result_text.config(state='normal') self.psnr_result_text.delete("1.0", tk.END) self.psnr_result_text.insert("1.0", result) if success and "PSNR:" in result: self.psnr_result_text.tag_configure("psnr", foreground=color, font=("Arial", 12, "bold")) self.psnr_result_text.tag_add("psnr", "1.0", "1.0 lineend") self.psnr_result_text.config(state='disabled') def calculate_psnr_pair(self, original, modified): """Расчёт PSNR для пары изображений (вызывается после кодирования)""" try: psnr = calculate_psnr(original, modified) if psnr == float('inf'): quality = "Идеальное (изображения идентичны)" elif psnr > 40: quality = "Отличное (изменения практически незаметны)" elif psnr > 30: quality = "Хорошее" elif psnr > 20: quality = "Заметные искажения" else: quality = "Сильные искажения" messagebox.showinfo( "PSNR - Качество изображения", f"PSNR: {psnr:.2f} dB\n\n" f"Оценка качества: {quality}\n\n" f"PSNR показывает, насколько сильно изменилось изображение\n" f"после внедрения сообщения. Чем выше значение, тем лучше." ) except Exception as e: messagebox.showerror("Ошибка", f"Не удалось рассчитать PSNR:\n{e}") def main(): """Запуск GUI""" root = tk.Tk() app = StegoGUI(root) root.mainloop() if __name__ == '__main__': main()