Compare commits
16 Commits
f053e2becc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d990b283ae | ||
|
|
cd82b9669a | ||
|
|
3b34b3e7ac | ||
|
|
975dea8ea4 | ||
|
|
a8141e3e7f | ||
|
|
336b10a6a7 | ||
|
|
7f26a909a1 | ||
|
|
296af19989 | ||
|
|
2221f52f60 | ||
|
|
ce2f18fa74 | ||
|
|
b173264cf4 | ||
|
|
d220fbcf89 | ||
|
|
867caff650 | ||
|
|
ce89043fb8 | ||
|
|
2f2117e240 | ||
|
|
4e604b73c8 |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
# Виртуальное окружение
|
||||
venv/
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
@@ -22,4 +23,11 @@ Thumbs.db
|
||||
test_image.png
|
||||
encoded_lsb.png
|
||||
test_*.png
|
||||
*_lsb.png
|
||||
*_lsb.png
|
||||
|
||||
# Временные файлы установки
|
||||
*.egg-info/
|
||||
*.egg
|
||||
build/
|
||||
dist/
|
||||
dct_smooth.png
|
||||
|
||||
107
README.md
107
README.md
@@ -1,2 +1,107 @@
|
||||
# CourseWork
|
||||
# Stego App
|
||||
|
||||
**Стеганографическое приложение для сокрытия данных в изображениях**
|
||||
|
||||
## Функциональность
|
||||
|
||||
| Метод | Описание |
|
||||
|-------|------------------------------------------------------------------------------------------------|
|
||||
|**LSB**|(Least Significant Bit) | Сокрытие в младших битах пикселей. Работает с любыми изображениями |
|
||||
|**DCT**|(Discrete Cosine Transform) | Сокрытие в частотной области. Для гладких/однотонных изображений |
|
||||
|
||||
## Возможности
|
||||
|
||||
- Графический интерфейс (Tkinter)
|
||||
- CLI интерфейс
|
||||
- Поддержка русских и английских текстов
|
||||
- Сохранение результатов на устройство
|
||||
- Расчёт вместимости изображения
|
||||
- Сравнение изображений (PSNR)
|
||||
|
||||
---
|
||||
# Получение кода
|
||||
|
||||
## Способ 1: Клонирование репозитория (если есть доступ)
|
||||
|
||||
git clone <ссылка_на_репозиторий>
|
||||
|
||||
cd CourseWork
|
||||
|
||||
### Что такое git clone?
|
||||
Это команда, которая скачивает копию репозитория с сервера (например, с Gitea, GitHub) на ваш компьютер.
|
||||
|
||||
### Где взять ссылку?
|
||||
В веб-интерфейсе репозитория нажмите кнопку Clone или Клонировать и скопируйте ссылку (HTTPS или SSH).
|
||||
|
||||
## Способ 2: Скачать архив с Gitea/GitHub
|
||||
1. Перейдите на страницу репозитория
|
||||
2. Нажмите кнопку Download ZIP или Скачать архив
|
||||
3. Распакуйте архив
|
||||
4. Откройте терминал в распакованной папке
|
||||
|
||||
## Установка
|
||||
|
||||
### Автоматическая установка (рекомендуется)
|
||||
chmod +x install.sh
|
||||
./install.sh
|
||||
|
||||
### Установка через pipx
|
||||
pipx install .
|
||||
|
||||
### Ручная установка зависимостей
|
||||
pip install numpy pillow scipy
|
||||
|
||||
### Установка Tkinter (Ubuntu/Debian)
|
||||
sudo apt install python3-tk
|
||||
|
||||
### Запуск
|
||||
|**Команда** |**Описание**|
|
||||
|
||||
|stego-gui |Графический интерфейс|
|
||||
|
||||
|stego --help| Помощь по CLI|
|
||||
|
||||
## CLI команды
|
||||
#### LSB кодирование
|
||||
stego encode lsb image.png out.png -m "Секретное сообщение"
|
||||
|
||||
#### LSB декодирование
|
||||
stego decode lsb out.png
|
||||
|
||||
#### DCT кодирование
|
||||
stego encode dct image.png out.png -m "Секретное сообщение"
|
||||
|
||||
#### DCT декодирование
|
||||
stego decode dct out.png
|
||||
|
||||
#### Показать вместимость изображения
|
||||
stego capacity image.png
|
||||
|
||||
#### Сравнить два изображения (PSNR)
|
||||
stego psnr original.png modified.png
|
||||
|
||||
## Удаление
|
||||
### Через pipx
|
||||
pipx uninstall stego-app
|
||||
|
||||
### Через скрипт
|
||||
chmod +x uninstall.sh
|
||||
./uninstall.sh
|
||||
|
||||
## Требования к системе
|
||||
ОС: Linux (Ubuntu/Debian и производные)
|
||||
|
||||
Python: 3.8 или выше
|
||||
|
||||
Зависимости: numpy, pillow, scipy, tkinter
|
||||
|
||||
## Известные ограничения
|
||||
DCT метод требует размеры изображения, кратные 8 (например, 400×400, 512×512, 800×800)
|
||||
|
||||
DCT метод рекомендуется только для гладких/однотонных изображений
|
||||
|
||||
Результат кодирования рекомендуется сохранять в формате PNG (без потерь)
|
||||
|
||||
### Лицензия
|
||||
MIT License
|
||||
---
|
||||
@@ -13,7 +13,7 @@ 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
|
||||
from core.utils import calculate_capacity, calculate_psnr, text_to_bits
|
||||
|
||||
|
||||
class StegoGUI:
|
||||
@@ -22,7 +22,7 @@ class StegoGUI:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("Stego App - Стеганографическое приложение")
|
||||
self.root.geometry("800x700")
|
||||
self.root.geometry("850x750")
|
||||
self.root.resizable(True, True)
|
||||
|
||||
# Переменные
|
||||
@@ -32,6 +32,10 @@ class StegoGUI:
|
||||
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):
|
||||
@@ -50,6 +54,11 @@ class StegoGUI:
|
||||
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="О программе")
|
||||
@@ -61,8 +70,8 @@ class StegoGUI:
|
||||
|
||||
# Метод
|
||||
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").grid(row=row, column=1, padx=5, sticky='w')
|
||||
ttk.Radiobutton(self.encode_frame, text="DCT (для гладких/одноцветных изображений)", variable=self.method_encode, value="dct").grid(row=row, column=2, padx=5, sticky='w')
|
||||
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
|
||||
|
||||
# Разделитель
|
||||
@@ -87,10 +96,16 @@ class StegoGUI:
|
||||
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
|
||||
|
||||
# Кнопка очистки сообщения
|
||||
@@ -101,7 +116,7 @@ class StegoGUI:
|
||||
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
|
||||
@@ -119,16 +134,16 @@ class StegoGUI:
|
||||
self.status_label.grid(row=row, column=0, columnspan=4, pady=5)
|
||||
|
||||
# Привязка событий
|
||||
self.encode_image_path.trace('w', lambda *args: self.update_capacity())
|
||||
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.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')
|
||||
ttk.Radiobutton(self.decode_frame, text="DCT (для гладких изображений)", variable=self.method_decode, value="dct").grid(row=row, column=2, padx=5, sticky='w')
|
||||
row += 1
|
||||
|
||||
# Разделитель
|
||||
@@ -137,8 +152,7 @@ class StegoGUI:
|
||||
|
||||
# Изображение
|
||||
ttk.Label(self.decode_frame, text="Изображение со скрытым сообщением:").grid(row=row, column=0, sticky='w', padx=5, pady=5)
|
||||
self.decode_image_entry = ttk.Entry(self.decode_frame, textvariable=self.decode_image_path, width=55)
|
||||
self.decode_image_entry.grid(row=row, column=1, columnspan=2, padx=5, sticky='ew')
|
||||
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
|
||||
|
||||
@@ -176,30 +190,64 @@ class StegoGUI:
|
||||
|
||||
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.Label(info_frame, text="2026", font=("Arial", 10, "italic")).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=" • 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')
|
||||
@@ -210,9 +258,14 @@ class StegoGUI:
|
||||
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=" • Для сохранения результата используйте PNG").pack(anchor='w')
|
||||
ttk.Label(tech_frame, text=" • LSB - универсальный метод, рекомендуется по умолчанию").pack(anchor='w')
|
||||
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):
|
||||
"""Выбор изображения для кодирования"""
|
||||
@@ -233,24 +286,10 @@ class StegoGUI:
|
||||
if filename:
|
||||
self.encode_output_path.set(filename)
|
||||
|
||||
def select_decode_image(self):
|
||||
"""Выбор изображения для декодирования"""
|
||||
filename = filedialog.askopenfilename(
|
||||
title="Выберите изображение со скрытым сообщением",
|
||||
filetypes=[("Изображения", "*.png *.bmp *.jpg *.jpeg"), ("Все файлы", "*.*")]
|
||||
)
|
||||
if filename:
|
||||
self.decode_image_path.set(filename)
|
||||
|
||||
def clear_message(self):
|
||||
"""Очистка поля сообщения"""
|
||||
self.message_text.delete("1.0", tk.END)
|
||||
|
||||
def clear_result(self):
|
||||
"""Очистка поля результата"""
|
||||
self.result_text.delete("1.0", tk.END)
|
||||
self.decoded_message = ""
|
||||
self.save_btn.config(state='disabled')
|
||||
self.update_char_counter()
|
||||
|
||||
def update_capacity(self):
|
||||
"""Обновление информации о вместимости"""
|
||||
@@ -265,6 +304,89 @@ class StegoGUI:
|
||||
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:
|
||||
@@ -273,13 +395,10 @@ class StegoGUI:
|
||||
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)
|
||||
# Если стандартное отклонение цветов большое (>50) — изображение пёстрое
|
||||
if max(stat.stddev) > 50:
|
||||
return False, "Изображение содержит много деталей/цветов.\nDCT рекомендуется только для гладких/однотонных изображений.\nИспользуйте LSB метод."
|
||||
|
||||
@@ -304,16 +423,27 @@ class StegoGUI:
|
||||
messagebox.showerror("Ошибка", "Введите сообщение для сокрытия")
|
||||
return
|
||||
|
||||
# Проверка для DCT
|
||||
# Проверка превышения лимита
|
||||
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=11, column=1, columnspan=2, sticky='ew', padx=5, pady=5)
|
||||
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")
|
||||
|
||||
@@ -329,11 +459,11 @@ class StegoGUI:
|
||||
else:
|
||||
success = encode_dct(image, message, output)
|
||||
|
||||
self.root.after(0, self._encode_done, success, None)
|
||||
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))
|
||||
self.root.after(0, self._encode_done, False, str(e), image, output)
|
||||
|
||||
def _encode_done(self, success, error=None):
|
||||
def _encode_done(self, success, error=None, original=None, modified=None):
|
||||
"""Завершение кодирования"""
|
||||
self.encode_progress.stop()
|
||||
self.encode_progress.grid_forget()
|
||||
@@ -341,12 +471,38 @@ class StegoGUI:
|
||||
|
||||
if success:
|
||||
self.status_label.config(text="✅ Кодирование успешно завершено!", foreground="green")
|
||||
messagebox.showinfo("Успех", f"✅ Сообщение успешно скрыто в изображении!\n\nСохранено: {self.encode_output_path.get()}")
|
||||
|
||||
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()
|
||||
@@ -356,9 +512,8 @@ class StegoGUI:
|
||||
messagebox.showerror("Ошибка", "Выберите изображение")
|
||||
return
|
||||
|
||||
# Запускаем в отдельном потоке
|
||||
self.decode_btn.config(state='disabled')
|
||||
self.decode_progress.grid(row=2, column=1, columnspan=2, sticky='ew', padx=5, pady=5)
|
||||
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")
|
||||
|
||||
@@ -414,6 +569,119 @@ class StegoGUI:
|
||||
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():
|
||||
|
||||
BIN
icons/stego.png
Normal file
BIN
icons/stego.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
137
install.sh
Executable file
137
install.sh
Executable file
@@ -0,0 +1,137 @@
|
||||
#!/bin/bash
|
||||
# install.sh - Установка Stego App
|
||||
# Запуск: chmod +x install.sh && ./install.sh
|
||||
|
||||
set -e
|
||||
|
||||
echo "=========================================="
|
||||
echo "Установка Stego App"
|
||||
echo "=========================================="
|
||||
|
||||
PROJECT_DIR="$(pwd)"
|
||||
USER_BIN="$HOME/.local/bin"
|
||||
|
||||
# 1. Проверка Python
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "❌ Python 3 не найден"
|
||||
echo "Установите Python 3.8+: sudo apt install python3 python3-pip python3-venv"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Python найден: $(python3 --version)"
|
||||
|
||||
# 2. Установка Tkinter (для GUI)
|
||||
if ! python3 -c "import tkinter" &> /dev/null; then
|
||||
echo "Установка Tkinter..."
|
||||
sudo apt update
|
||||
sudo apt install python3-tk -y
|
||||
fi
|
||||
echo "✓ Tkinter установлен"
|
||||
|
||||
# 3. Создание виртуального окружения
|
||||
echo "Создание виртуального окружения..."
|
||||
if [ -d ".venv" ]; then
|
||||
echo "Виртуальное окружение уже существует, обновляем..."
|
||||
rm -rf .venv
|
||||
fi
|
||||
python3 -m venv .venv
|
||||
echo "✓ Виртуальное окружение создано"
|
||||
|
||||
# 4. Установка зависимостей
|
||||
echo "Установка зависимостей..."
|
||||
source .venv/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install numpy pillow scipy
|
||||
deactivate
|
||||
echo "✓ Зависимости установлены"
|
||||
|
||||
# 5. Создание запускаторов
|
||||
echo "Создание запускаторов..."
|
||||
mkdir -p "$USER_BIN"
|
||||
|
||||
cat > "$USER_BIN/stego" << EOF
|
||||
#!/bin/bash
|
||||
cd "$PROJECT_DIR"
|
||||
source .venv/bin/activate
|
||||
python main.py "\$@"
|
||||
EOF
|
||||
|
||||
cat > "$USER_BIN/stego-gui" << EOF
|
||||
#!/bin/bash
|
||||
cd "$PROJECT_DIR"
|
||||
source .venv/bin/activate
|
||||
python main.py --gui
|
||||
EOF
|
||||
|
||||
chmod +x "$USER_BIN/stego"
|
||||
chmod +x "$USER_BIN/stego-gui"
|
||||
echo "✓ Запускаторы созданы: stego, stego-gui"
|
||||
|
||||
# 6. Добавление ~/.local/bin в PATH (если нужно)
|
||||
if [[ ":$PATH:" != *":$USER_BIN:"* ]]; then
|
||||
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
|
||||
echo "✓ PATH обновлён. Перезагрузите терминал или выполните: source ~/.bashrc"
|
||||
fi
|
||||
|
||||
# 7. Установка иконки
|
||||
echo "Установка иконки..."
|
||||
if [ -f "icons/stego.png" ]; then
|
||||
sudo mkdir -p /usr/local/share/stego/icons
|
||||
sudo cp icons/stego.png /usr/local/share/stego/icons/
|
||||
echo "✓ Иконка установлена"
|
||||
else
|
||||
echo "⚠️ Иконка не найдена: icons/stego.png"
|
||||
fi
|
||||
|
||||
# 8. Установка .desktop файла (ярлык в меню)
|
||||
echo "Установка ярлыка в меню..."
|
||||
mkdir -p ~/.local/share/applications
|
||||
cat > ~/.local/share/applications/stego.desktop << DESKTOP
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Stego App
|
||||
Comment=Сокрытие данных в изображениях
|
||||
Exec=$USER_BIN/stego-gui
|
||||
Icon=/usr/local/share/stego/icons/stego.png
|
||||
Terminal=false
|
||||
Categories=Graphics;Utility;
|
||||
StartupNotify=true
|
||||
DESKTOP
|
||||
|
||||
# 9. Ярлык на рабочий стол (опционально)
|
||||
read -p "Создать ярлык на рабочем столе? (y/n): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
cat > ~/Desktop/stego.desktop << DESKTOP
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Stego App
|
||||
Comment=Сокрытие данных в изображениях
|
||||
Exec=$USER_BIN/stego-gui
|
||||
Icon=/usr/local/share/stego/icons/stego.png
|
||||
Terminal=false
|
||||
Categories=Graphics;
|
||||
StartupNotify=true
|
||||
DESKTOP
|
||||
chmod +x ~/Desktop/stego.desktop
|
||||
echo "✓ Ярлык создан на рабочем столе"
|
||||
fi
|
||||
|
||||
# Обновление базы приложений
|
||||
update-desktop-database ~/.local/share/applications/ 2>/dev/null || true
|
||||
update-desktop-database /usr/share/applications/ 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "✅ Установка завершена!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Запуск GUI: stego-gui"
|
||||
echo "CLI справка: stego --help"
|
||||
echo "LSB кодирование: stego encode lsb image.png out.png -m \"Сообщение\""
|
||||
echo "LSB декодирование: stego decode lsb out.png"
|
||||
echo ""
|
||||
echo "Ярлык появится в меню приложений (Graphics/Stego App)"
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Ярлык также создан на рабочем столе"
|
||||
fi
|
||||
echo "=========================================="
|
||||
8
main.py
8
main.py
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CLI интерфейс для стеганографических методов LSB и DCT.
|
||||
CLI интерфейс для методов LSB и DCT.
|
||||
|
||||
Использование:
|
||||
python main.py encode <method> <image> <output> [-m MESSAGE] [-f FILE]
|
||||
@@ -211,10 +211,8 @@ def gui_main():
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
if len(sys.argv) > 1 and sys.argv[1] == '--gui':
|
||||
gui_main()
|
||||
else:
|
||||
main()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
main()
|
||||
24
setup.py
Normal file
24
setup.py
Normal file
@@ -0,0 +1,24 @@
|
||||
""" Основа установки и работы приложения """
|
||||
#!/usr/bin/env python3
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name='stego-app',
|
||||
version='1.0.0',
|
||||
author='Петров Игорь ИУ10-24',
|
||||
description='Steganography application for hiding data in images',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=[
|
||||
'numpy>=2.4.3',
|
||||
'pillow>=12.1.1',
|
||||
'scipy>=1.7.0',
|
||||
],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'stego = main:main',
|
||||
'stego-gui = main:gui_main',
|
||||
],
|
||||
},
|
||||
python_requires='>=3.8',
|
||||
)
|
||||
10
stego.desktop
Normal file
10
stego.desktop
Normal file
@@ -0,0 +1,10 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Stego App
|
||||
Comment=Сокрытие данных в изображениях
|
||||
Exec=stego-gui
|
||||
Icon=stego
|
||||
Terminal=false
|
||||
Categories=Graphics;Utility;
|
||||
Keywords=steganography;hide;image;
|
||||
StartupNotify=true
|
||||
71
uninstall.sh
Executable file
71
uninstall.sh
Executable file
@@ -0,0 +1,71 @@
|
||||
#!/bin/bash
|
||||
# uninstall.sh - Удаление Stego App
|
||||
# Запуск: chmod +x uninstall.sh && ./uninstall.sh
|
||||
|
||||
set -e
|
||||
|
||||
echo "=========================================="
|
||||
echo "Удаление Stego App"
|
||||
echo "=========================================="
|
||||
|
||||
PROJECT_DIR="$(pwd)"
|
||||
USER_BIN="$HOME/.local/bin"
|
||||
|
||||
# 1. Удаление запускаторов
|
||||
echo "Удаление запускаторов..."
|
||||
rm -f "$USER_BIN/stego"
|
||||
rm -f "$USER_BIN/stego-gui"
|
||||
echo "✓ Запускаторы удалены"
|
||||
|
||||
# 2. Удаление иконки
|
||||
echo "Удаление иконки..."
|
||||
sudo rm -rf /usr/local/share/stego
|
||||
echo "✓ Иконка удалена"
|
||||
|
||||
# 3. Удаление .desktop файла из меню
|
||||
echo "Удаление ярлыка из меню..."
|
||||
rm -f ~/.local/share/applications/stego.desktop
|
||||
update-desktop-database ~/.local/share/applications/ 2>/dev/null || true
|
||||
echo "✓ Ярлык из меню удалён"
|
||||
|
||||
# 4. Удаление ярлыка с рабочего стола (если есть)
|
||||
echo "Удаление ярлыка с рабочего стола..."
|
||||
rm -f ~/Desktop/stego.desktop
|
||||
rm -f ~/Рабочий\ стол/stego.desktop
|
||||
echo "✓ Ярлык с рабочего стола удалён"
|
||||
|
||||
# 5. Удаление виртуального окружения (опционально)
|
||||
read -p "Удалить виртуальное окружение (.venv)? (y/n): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
rm -rf "$PROJECT_DIR/.venv"
|
||||
echo "✓ Виртуальное окружение удалено"
|
||||
else
|
||||
echo "Виртуальное окружение сохранено"
|
||||
fi
|
||||
|
||||
# 6. Удаление через pipx (если установлено, без ошибок)
|
||||
if command -v pipx &> /dev/null; then
|
||||
if pipx list 2>/dev/null | grep -q "stego-app"; then
|
||||
pipx uninstall stego-app
|
||||
echo "✓ Пакет stego-app удалён через pipx"
|
||||
else
|
||||
echo "ℹ️ Пакет stego-app не найден в pipx (пропускаем)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 7. Очистка кэша Python
|
||||
echo "Очистка кэша Python..."
|
||||
find "$PROJECT_DIR" -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
||||
find "$PROJECT_DIR" -type f -name "*.pyc" -delete 2>/dev/null || true
|
||||
echo "✓ Кэш очищен"
|
||||
|
||||
# 8. Очистка PATH от ~/.local/bin (только если ничего не осталось)
|
||||
if [ ! -f "$USER_BIN/stego" ] && [ ! -f "$USER_BIN/stego-gui" ]; then
|
||||
echo "ℹ️ Запускаторы удалены"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "✅ Удаление завершено!"
|
||||
echo "=========================================="
|
||||
Reference in New Issue
Block a user