695 lines
34 KiB
Python
695 lines
34 KiB
Python
"""
|
||
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() |