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

Зачем это нужно? Приватность и творчество
Идея прятать один файл в другом не нова. Но большинство утилит созданы для специалистов. Мы же сфокусировались на практическом применении для всех.
Самое главное — конфиденциальность. В цифровую эпоху сложно быть уверенным в приватности пересылаемых данных. Наш плеер предлагает решение: вы можете отправить фотографию через любой открытый канал (почту, соцсеть), и только тот, у кого есть приложение ChameleonLab, сможет узнать о существовании скрытого аудиосообщения и прослушать его. Это идеальный способ передать личную информацию, не вызывая подозрений.
"Живые" фотоальбомы: Сохраняйте короткие аудиозаметки или окружающие звуки прямо в ваших фотографиях. Фотография ребенка с его первым словом, снимок с концерта с фрагментом выступления, фото с дня рождения с поздравлениями — все это хранится в одном PNG-файле, скрытое от посторонних.
Образование и искусство: Представьте интерактивную выставку в музее или урок ИЗО. Ученики открывают на планшетах репродукции картин, и каждая картина "рассказывает" голосом экскурсовода о своей истории, авторе и технике.
Как это работает: Погружение в код
В основе всего лежит классический метод стеганографии LSB (Least Significant Bit). Если кратко, мы берем наименее значимые биты каждого цветового компонента (R, G, B) каждого пикселя и заменяем их битами нашего аудиофайла. Для человеческого глаза эти изменения абсолютно незаметны.
В качестве контейнера мы используем формат PNG, потому что он сжимает данные без потерь. Использование JPG для этих целей губительно, так как его алгоритмы сжатия с потерями разрушат и исказят спрятанную информацию.
В нашем приложении есть два ключевых компонента: Создатель и Плеер.
Шаг за шагом: Создаем наше первое аудио-фото
Мы встроили в плеер вкладку "Создатель", которая делает процесс максимально простым.
Выбираем изображение-контейнер. Можно перетащить файл в левое окно. Поддерживаются PNG, JPG, BMP. Любой формат на выходе будет конвертирован в PNG.
Выбираем аудиофайл. В правое окно перетаскиваем аудиофайл (
.mp3
или.wav
).Проверяем вместимость. Программа автоматически рассчитывает, поместится ли аудиофайл в картинку. Если нет, можно поставить галочку "Автоматически расширять...", и программа добавит к изображению снизу черные пиксели, чтобы увеличить его емкость, не искажая оригинал.
Создаем! Нажимаем кнопку "Создать и сохранить", и получаем наш гибридный PNG-файл.

Под капотом «Создателя»
За этот процесс отвечает фоновый воркер 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))
Сердце плеера: Как извлечь и проиграть звук
Процесс воспроизведения обратный и тоже выполняется в фоновом потоке, чтобы интерфейс не зависал.
Мгновенный предпросмотр: Как только пользователь выбирает трек, мы сразу загружаем и отображаем картинку.
Извлечение в фоне: Одновременно запускается
PlayerRevealWorker
. Он открывает PNG, считывает LSB-биты пикселей и восстанавливает из них спрятанный пакет данных ([имя файла]|[аудио]
).Воспроизведение: Когда воркер завершает работу, он передает извлеченные аудиобайты основному потоку. Мы сохраняем эти байты во временный файл на диске и передаем его стандартному
QMediaPlayer
для воспроизведения.Очистка: Временный файл автоматически удаляется после проигрывания или при закрытии программы.
Вот как выглядит воркер для извлечения:
# Из файла 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. Огромное спасибо за ваше участие и помощь!
Скачать:
Скачать последнюю версию на Windows: ChameleonLab 1.4.0.0
Скачать последнюю версию на macOS: ChameleonLab 1.4.0.0
Наш Telegram-канал: t.me/ChameleonLab