Привет, Хабр!

Вы когда-нибудь хотели, чтобы ваши фотографии могли рассказывать истории? Не в переносном смысле, а буквально. А что, если бы эти истории были предназначены только для вас? Представьте, что вы отправляете другу обычный с виду PNG-файл, но внутри него скрыто личное аудиопоздравление, которое не увидит ни один почтовый сервис или мессенджер. Или ведете цифровой фотодневник, где за каждым снимком скрывается голосовая заметка с вашими мыслями, надежно спрятанная от посторонних глаз.

Это не магия, а стеганография. Сегодня я расскажу о проекте ChameleonLab, а точнее — о его уникальной функции: стеганографическом имидж-плеере. Это десктопное приложение, которое позволяет не только прятать аудиофайлы внутри изображений, но и проигрывать их, как в обычном плеере, создавая новый способ для приватного и творческого обмена информацией. Проект уже имеет готовые сборки для Windows и macOS.

Программа "ChameleonLab". Стеганографический имидж-плеер
Программа "ChameleonLab". Стеганографический имидж-плеер

Зачем это нужно? Приватность и творчество

Идея прятать один файл в другом не нова. Но большинство утилит созданы для специалистов. Мы же сфокусировались на практическом применении для всех.

  • Самое главное — конфиденциальность. В цифровую эпоху сложно быть уверенным в приватности пересылаемых данных. Наш плеер предлагает решение: вы можете отправить фотографию через любой открытый канал (почту, соцсеть), и только тот, у кого есть приложение ChameleonLab, сможет узнать о существовании скрытого аудиосообщения и прослушать его. Это идеальный способ передать личную информацию, не вызывая подозрений.

  • "Живые" фотоальбомы: Сохраняйте короткие аудиозаметки или окружающие звуки прямо в ваших фотографиях. Фотография ребенка с его первым словом, снимок с концерта с фрагментом выступления, фото с дня рождения с поздравлениями — все это хранится в одном PNG-файле, скрытое от посторонних.

  • Образование и искусство: Представьте интерактивную выставку в музее или урок ИЗО. Ученики открывают на планшетах репродукции картин, и каждая картина "рассказывает" голосом экскурсовода о своей истории, авторе и технике.

Как это работает: Погружение в код

В основе всего лежит классический метод стеганографии LSB (Least Significant Bit). Если кратко, мы берем наименее значимые биты каждого цветового компонента (R, G, B) каждого пикселя и заменяем их битами нашего аудиофайла. Для человеческого глаза эти изменения абсолютно незаметны.

В качестве контейнера мы используем формат PNG, потому что он сжимает данные без потерь. Использование JPG для этих целей губительно, так как его алгоритмы сжатия с потерями разрушат и исказят спрятанную информацию.

В нашем приложении есть два ключевых компонента: Создатель и Плеер.

Шаг за шагом: Создаем наше первое аудио-фото

Мы встроили в плеер вкладку "Создатель", которая делает процесс максимально простым.

  1. Выбираем изображение-контейнер. Можно перетащить файл в левое окно. Поддерживаются PNG, JPG, BMP. Любой формат на выходе будет конвертирован в PNG.

  2. Выбираем аудиофайл. В правое окно перетаскиваем аудиофайл (.mp3 или .wav).

  3. Проверяем вместимость. Программа автоматически рассчитывает, поместится ли аудиофайл в картинку. Если нет, можно поставить галочку "Автоматически расширять...", и программа добавит к изображению снизу черные пиксели, чтобы увеличить его емкость, не искажая оригинал.

  4. Создаем! Нажимаем кнопку "Создать и сохранить", и получаем наш гибридный PNG-файл.

Программа "ChameleonLab". Создатель
Программа "ChameleonLab". Создатель

Под капотом «Создателя»

За этот процесс отвечает фоновый воркер PlayerCreateWorker. Главная работа происходит в функции steganography_core.hide(). Перед тем как спрятать аудио, мы формируем "полезную нагрузку" (payload) по простому формату:

[Имя файла в UTF-8] + [Символ-разделитель '|'] + [Байты аудиофайла]

Это позволяет нам при извлечении узнать оригинальное имя файла. Воркер в фоновом потоке выполняет всю тяжелую работу: расширяет изображение (если нужно), читает аудиофайл и вызывает stego.hide(), чтобы побитово вписать данные в пиксели.

Вот ключевая часть кода из workers.py:

# Из файла ui/workers.py
class PlayerCreateWorker(QtCore.QObject):
    # ... сигналы ...
    def __init__(self, carrier_data, audio_path, n_bits, should_pad):
        # ...
    
    def run(self):
        try:
            # ... расчет необходимого размера ...

            if required_size > capacity_bytes:
                if not self.should_pad:
                    raise ValueError(t("embed_log_conclusion_fail"))
                
                # Логика расширения холста изображения
                h, w, c = self.carrier_data.shape
                # ... расчет новых размеров ...
                new_h = math.ceil(required_pixels / w)
                padded_image = np.zeros((new_h, w, c), dtype=np.uint8)
                padded_image[0:h, :, :] = self.carrier_data
                self.carrier_data = padded_image
            
            self.progress.emit(40)
            
            with open(self.audio_path, 'rb') as f:
                audio_bytes = f.read()
            
            secret_filename = Path(self.audio_path).name
            packaged_data = secret_filename.encode('utf-8') + b'|' + audio_bytes
            
            self.progress.emit(60)
            output_payload = stego.hide(self.carrier_data, packaged_data, self.n_bits, is_encrypted=False)
            self.progress.emit(95)
            self.finished.emit(output_payload)

        except Exception as e:
            self.error.emit(str(e))

Сердце плеера: Как извлечь и проиграть звук

Процесс воспроизведения обратный и тоже выполняется в фоновом потоке, чтобы интерфейс не зависал.

  1. Мгновенный предпросмотр: Как только пользователь выбирает трек, мы сразу загружаем и отображаем картинку.

  2. Извлечение в фоне: Одновременно запускается PlayerRevealWorker. Он открывает PNG, считывает LSB-биты пикселей и восстанавливает из них спрятанный пакет данных ([имя файла]|[аудио]).

  3. Воспроизведение: Когда воркер завершает работу, он передает извлеченные аудиобайты основному потоку. Мы сохраняем эти байты во временный файл на диске и передаем его стандартному QMediaPlayer для воспроизведения.

  4. Очистка: Временный файл автоматически удаляется после проигрывания или при закрытии программы.

Вот как выглядит воркер для извлечения:

# Из файла ui/workers.py
class PlayerRevealWorker(QtCore.QObject):
    finished = QtCore.pyqtSignal(bytes) # audio_bytes
    error = QtCore.pyqtSignal(str)
    progress = QtCore.pyqtSignal(int)

    def __init__(self, image_path):
        super().__init__()
        self.image_path = image_path

    def run(self):
        try:
            self.progress.emit(20)
            carrier_data, _, _ = file_handlers.read_file(self.image_path)
            self.progress.emit(50)
            packaged_data, _, found = stego.reveal(carrier_data)
            self.progress.emit(90)

            if not found:
                raise ValueError(t("player_error_no_audio"))

            try:
                _, audio_bytes = packaged_data.split(b'|', 1)
            except ValueError:
                audio_bytes = packaged_data
            
            self.finished.emit(audio_bytes)
        except Exception as e:
            self.error.emit(str(e))

Трудности и решения

В процессе разработки мы столкнулись с несколькими классическими проблемами:

  • Сбои потоков: QThread: Destroyed while thread is still running — эта головная боль всех, кто работает с многопоточностью в PyQt. Решилась установкой родительского виджета для QThread (QtCore.QThread(self)), что создает жесткую связь и не дает сборщику мусора удалить объект потока раньше времени.

  • Зависание интерфейса: Изначально все операции выполнялись в основном потоке, что приводило к зависанию приложения на несколько секунд. Перенос всей тяжелой логики в классы-воркеры (QObject) и запуск их через QThread полностью решил эту проблему, сделав интерфейс отзывчивым.

Заключение

Проект ChameleonLab и его имидж-плеер — это пример того, как можно взять известную технологию и найти для нее новое, творческое и, что самое важное, приватное применение. Мы получили не просто утилиту, а интуитивно понятный инструмент для создания нового типа контента, где у каждого изображения есть второй, скрытый аудио-слой.

Это не только инструмент для творчества, позволяющий создавать "живые" фотографии, но и способ защитить личные аудиовоспоминания и сообщения в нашем излишне открытом цифровом мире.

Проект ChameleonLab уже доступен в виде готовых сборок для Windows и macOS, позволяя каждому желающему попробовать создать свои собственные "живые" и секретные фотографии уже сегодня.

Мы продолжим прислушиваться к вам и развивать ChameleonLab. Огромное спасибо за ваше участие и помощь!

Скачать:

Комментарии (5)


  1. Lomakn Автор
    30.08.2025 09:04

    Может кто подскажет из сообщества как реализовать DCT рабочий метод для JPG на Python. У нас проблема на шаге извлечение.


  1. Demmidovich
    30.08.2025 09:04

    Добавил в закладки. Действительно реализовали интересную идею. Ещё можно добавить и текстовый просмоторщик. Спасибо!


    1. Lomakn Автор
      30.08.2025 09:04

      Спасибо за обратный отзыв. Идею с текстом ещё в проигрыватель реализуем. Пока работаем над устранение багов.


  1. Artiik
    30.08.2025 09:04

    А получается можно использовать формат bmp


    1. Lomakn Автор
      30.08.2025 09:04

      Да можно любую картинку. На выходе будет PNG файл