Files
CourseWork/gui/main_window.py
Likaon a8141e3e7f Функция PSNR добавлена в gui
добавлена шкала заполнения текстом
2026-05-26 22:26:35 +03:00

695 lines
34 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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('<KeyRelease>', 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()