Стеганографические эксперименты с видеофайлами и Youtube

После продолжительного молчания маленький человек обратился к своему спутнику:
— Где умный человек прячет гальку?
— На морском берегу,  — низким голосом отозвался тот.
Маленький человек кивнул и после небольшой паузы спросил:
— А где умный человек прячет лист?
— В лесу,  — последовал ответ.

Гилберт Кийт Честертон, Сломанная шпага

Сможет ли собственная стеганографическая pet-поделка выдержать тесты и успешно пройти через жернова внутренних верификаций и преобразований Youtube, который решено было выбрать в качестве видеохостинга для наукообразных экспериментов? Можно ли в конечном итоге использовать Youtube для альтернативного хранения видеоданных? Данная заметка постарается если не закрыть полностью ответы на эти вопросы, то по крайней мере через натурный эксперимент проиллюстрировать потенциальные возможности, которые могут оказаться скрытыми за простыми предположениям относительно организации хранения и обработки видеоданных.

Формат современного видеофайла обусловлен, что ожидаемо, естественным стремлением инженеров к обеспечению баланса между качеством изображения, с одной стороны, и минимизации его размера, — с другой. Последняя особенность самым негативным образом будет сказываться, что также вполне ожидаемо, на стремлении заполнить файл в общем случае бинарными данными, поскольку работа алгоритмов компрессии неминуемо приведет к потере целостности и полноты этих данных. Если для бинарных данных это критично, то для видео-данных потеря отдельных деталей в изображении не столь принципиальна. С потерей некоторого процента информации вполне можно примериться. Ведь для любой стеганографической модификации требуется заплатить соответствующую цену и это нормальное положение вещей.

Представим исходный видеофайл через последовательность кадриков (фреймов) в формате PNG. Теперь с каждым таким фреймом можно работать точно также как с отдельным изображением. Первое, что приходит на мысль:, а что если хаотично рассеять пикселы изображения-донора среди пикселов изображения-акцептора?

Проведем первый эксперимент, целью которого выступит оценка визуального качества результата размешивания в картинке-акцепторе 1280×720 PNG картинки-донора 320×180 PNG. Соотношение размеров выбрано с таким расчетом, чтобы подмешиваемая информация «не сильно» искажала картинку, «не бросалась в глаза».

Картинка-акцептор. https://www.vidsplay.com/peoplenyc-free-stock-video/Картинка-акцептор. https://www.vidsplay.com/peoplenyc-free-stock-video/Картинка-донор. https://www.vidsplay.com/subway-free-stock-video/Картинка-донор. https://www.vidsplay.com/subway-free-stock-video/

Для размешивания используем следующий наивный подход. Возьмём псевдослучайный генератор целых чисел с инициализацией seed = некоторая константа, которая при восстановлении изображения позволит воссоздать точную последовательность наших псевдослучайных параметров. Будем генерировать случайные координаты (x, y) в диапазоне размеров изображения-акцептора. Если пиксел-точка (x, y) встретится в процессе генерирования вновь, то вновь сгенерируем эти координаты. Повторим генерацию до тех пор, пока все (x, y) не выпадут с уникальными значениями. Размешивание на языке Pyhton примет вид:

from PIL import Image
from random import seed, randint


def mix_two(one_image, two_image, save_image, s=101):

    seed(s)

    crc = 0.0

    hash_xy = {}

    with Image.open(one_image).convert("RGBA") as one, Image.open(two_image).convert(
        "RGBA"
    ) as two:

        width_one, height_one = one.size
        width_two, height_two = two.size

        for x_two in range(width_two):
            for y_two in range(height_two):

                r, g, b, a = two.getpixel((x_two, y_two))

                while True:

                    x_one = randint(0, width_one - 1)
                    y_one = randint(0, height_one - 1)

                    key_xy = y_one * width_one + x_one

                    if key_xy in hash_xy.keys():
                        continue

                    crc += (0.2627 * r + 0.678 * g + 0.0593 * b) * key_xy

                    hash_xy[key_xy] = True

                    one.putpixel((x_one, y_one), (r, g, b, a))

                    break

        one.save(save_image, "PNG")

        return round(crc)

Таким образом каждому пикселу изображения донора будет поставлен в уникальное соответствие пиксел акцептора. Выполним замену пикселей в изображении-акцепторе пикселями-донорами. Сразу отметим, забегая вперед, что здесь допустимы и даже крайне желательны различные варианты реализации этого похода и творческие идеи, которые по вполне объяснимым причинам ограничиваются лишь глубиной фантазии программиста и спецификой поставленной задачи. Например, можно записывать донор сразу в несколько файлов-акцепторов. Один фрейм донора можно «растянуть» на несколько фреймов акцептора, причем последовательность и принципы хранения могут быть самыми разнообразными, с использованием шифрования при необходимости, различной избыточностью хранения, например, для борьбы с потерей качества и т.д. и т.п. Свобода и богатство выбора этих параметров и методов позволяет надеяться, что при аккуратном техническом исполнении в дальнейшем артефакты на изображении могут быть полностью нивелированы, а процесс восстановления встраиваемого видео станет еще проще и эффективнее.

Результат смешиванияРезультат смешивания

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

Уменьшенное изображениеУменьшенное изображение

Если в видеоплеере изображение будет соответствующим образом отмасштабировано, то артефакты на нём станут менее заметными. Шум может быть убран также в процессе воспроизведения blur-фильтром или каким-либо иным инструментом. Гипотетически, если обучить нейронную сеть на исходном видео, то с шумом также можно попробовать побороться. Однако это априори вычислительно-затратные операции, к тому же требующие к себе особого внимания, поэтому они лежат сейчас вне нашего научного интереса.

Поскольку алгоритмы видеокомпрессии активно работают с частотными характеристиками элементов на изображении, то необходимо сделать что-то в целях стабилизации присутствия встраиваемых пикселей. Очередной наивный подход: предлагается несколько раз повторить подряд в неизменном виде каждый фрейм со смешанным изображением. Для нашего видео будем повторять фреймы 25 раз подряд. Это приведет к замедлению воспроизведения результата в нормальном режиме, однако для демонстрационных целей можно в будущем скорректировать скорость воспроизведения так, чтобы эффект замедления исчез совсем. Если не требуется скрывать факт внедрения дополнительной информации, как в нашем случае, замедление вполне терпимое явление. Для восстанавливаемого видео еще проще: можно скорректировать скорость в процессе его сборки из отдельных фреймов. Обгоняя повествование представим эту реализацию при помощи ffmpeg:

ffmpeg -filter_complex [0:v]setpts=0.0346*PTS -pattern_type glob -i "./frames5/*.png" 5.webm

Магический коэффициент 0.0346 можно подкрутить с таким расчетом, чтобы файл-донор и восстанавливаемый файл имели одинаковую длительность.

После объединения фреймов в целостный файл:

ffmpeg -pattern_type glob -i "./frames3/*.png" 3.webm

Попробуем разместить его на Youtube, а потом получить свой файл обратно в максимально хорошем качестве для научного анализа:

Теперь скачанный ролик порежем на фреймы, как и прежде:

ffmpeg -i 4.webm "./frames4/out-%8d.png"

А для каждого фрейма запустим функцию восстановления:

def extract_two(one_image, width_two, height_two, save_image, s=101):

    seed(s)

    crc = 0.0

    hash_xy = {}

    with Image.open(one_image).convert("RGBA") as one, Image.new(
        "RGBA", (width_two, height_two)
    ) as two:

        width_one, height_one = one.size

        for x_two in range(width_two):
            for y_two in range(height_two):

                while True:

                    x_one = randint(0, width_one - 1)
                    y_one = randint(0, height_one - 1)

                    key_xy = y_one * width_one + x_one

                    if key_xy in hash_xy.keys():
                        continue

                    hash_xy[key_xy] = True

                    r, g, b, a = one.getpixel((x_one, y_one))

                    crc += (0.2627 * r + 0.678 * g + 0.0593 * b) * key_xy

                    two.putpixel((x_two, y_two), (r, g, b, a))

                    break

        two.save(save_image, "PNG")

        return round(crc)

Но перед этим проверим контрольные суммы размещенного файла на Youtube и скачанного, чтобы подтвердить факт того, что Youtube перемолол наш файл своими жерновами.

Восстановленные фреймы объединим в отдельный файл и оценим его качество визуально:

Здесь слева — видео-донор, справа — восстановленное видео. Как видим, качество пострадало не фатально. То есть мы добились того, чего собственно желали.

Правовой аспект

Стеганография подразумевает работу с определенной информацией с учётом сохранения в тайне самого факта хранения такой информации. Однако в научных целях мы намеренно не скрываем факт размещения дополнительной информации, а наоборот, — делаем этот факт публичным, чтобы в максимальной полноте соответствовать принципу добросовестного использования, указывая необходимые ссылки и добавляя соответствующие пояснения, отчуждая в общественное использование все необходимые материалы для воспроизведения результатов. Мы не вводим никого в заблуждение и уважаем права правообладателей. Вместе с этим призываем также поступать со всеми промежуточными и конечными материалами, которые могут быть порождены в результате использования скриптов, прилагаемых к этим заметкам. Относительно скачиваемого видеофайла с Youtube следует отдельно пояснить, что он сгенерирован лично мною. Файл был загружен добровольно и не обременен какими-либо авторскими правами, его донорные файлы были скачаны из легальных открытых и бесплатных источников и используются в научных, а не коммерческих целях. Поскольку существует раздел «Как скачать видео со своего канала» логично предположить, что выкачивание видеофайла со своего канала, согласно правилам Youtube, является легитимным действием.

Заключение

  1. Крайне примитивный авторский стеганографический подход продемонстрировал свою практическую эффективность. Жернова Youtube не смогли перемолоть загружаемый видео-файл настолько, чтобы из него нельзя было выкристаллизовать примешиваемый видео-файл более или менее адекватного цели эксперимента качества, конечно же при определенных технических компромиссах и алгоритмических упрощениях. Например в текущем эксперименте упускается работа со звуком, который, однако, также требует к себе специального внимания.

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

Литература для размышления

Hide Data Inside an Image using LSB Steganography With Python.

Steganography. From Wikipedia

Blind image watermarking method based on chaotic key and dynamic coefficient quantization in the DWT domain.

Diffusion models are autoencoders.

P.S.

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

© Habrahabr.ru