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

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

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

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

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

Проведем первый эксперимент, целью которого выступит оценка визуального качества результата размешивания в картинке-акцепторе 1280x720 PNG картинки-донора 320x180 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.

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

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


  1. amarao
    16.02.2022 14:25
    +3

    Мне кажется, что подмешивать что-то в сигнал в раскодированном виде - это бороться с кодеками. Надо не бороться, а возглавить. После того, как изображение уже пожато можно посмотреть на результаты сжатия и найти места, где можно "дописать" или "заменить", аналогичными блоками второго изображения. Т.е. вместо борьбы с кодированием остаётся борьба с перекодированием (которое, возможно, будет работать с удобным для себя представлением и сохранит большую часть модификаций).


    1. aleksiej-ostrowski Автор
      16.02.2022 19:14
      +2

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


  1. panteleymonov
    16.02.2022 14:29

    Не берусь говорить за сокрытие данных в видеопотоке, но на примере JPG алгоритмов данные можно легко и без потери качества оригинала сохранить в битах частотных коэффициентов. Алгоритм упаковки таких коэффициентов позволяет менять некоторые его биты, а для сохранения качества лучше всего делать это с младшими. По аналогу можно попробовать сохранить информацию и в видео потоке, но объем спрятанной информации при таком подходе не велик. Зато визуально не портит картинку, качество и размер изначальных файлов не меняется. Алгоритм сам по себе не требует полной реализации декодера, требуется только первоначальный (конечный) этап распаковки (сжатия).

    После перекодирования, конечно же, все данные теряются.


    1. savostin
      16.02.2022 18:59

      Как я понимаю, автор с неминуемым и неумолимым перекодированием видео при заливке на Youtube и борется.


      1. aleksiej-ostrowski Автор
        16.02.2022 19:43

        Совершенно верно.


    1. aleksiej-ostrowski Автор
      16.02.2022 19:17

      Солидарен с вами.