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

Меня данная технология заинтересовала недавно. Впервые о ней я узнал из доклада одного из спикеров на “AI Conference 2018”. Там демонстрировалось видео, в котором по аудиозаписи алгоритм сгенерировал видео с обращением Барака Обамы. Ссылка на подборку видео созданных с помощью этой технологии. Результаты меня сильно вдохновили, и мною было принято решение лучше разобраться с данной технологией, чтобы в будущем противодействовать ей. Для этого я решил написать DeepFake на языке C#. В итоге получил такой результат.

image

Приятного чтения!

Общие принципы

Отправной точкой стал этот проект. Из него я узнал как именно работает замена лица на видео.

  1. Загрузка картинки с которой мы будем брать лицо
  2. Извлечение лица
  3. Создание 3D маски
  4. Видео разбивается на кадры
  5. Вычисляется область локализации лица в кадре
  6. Вычисляется ракурс и выражение лица
  7. Перенос поворота и выражения лица на 3D модель
  8. Рендеринг
  9. Замена реального лица на кадре результатом рендеринга

Видео с демонстрацией работы проекта «FaceSwap»:


Работу я решил разбить на 3 части:

1-я) Замена лица на одном фото лицом с другого, без использования 3D маски
2-я) Доработка замены с применением 3D маски
3-я) Обработка видео

Замену лица на фото можно разложить на следующие пункты:

  1. Загрузка картинки с которой мы будем брать лицо
  2. Загрузка картинки на которую будем проецировать лицо
  3. Извлечение лиц
  4. Масштабирование лица взятого с изображения 2 к пропорциям в изображении 1
  5. Замена лица в картинке 1 на лицо в картинке 2

Встраивание одного изображения в другое

Первое с чего я начал работу — это встраивание одного изображения в другое. Для демонстрации встраивания в оригинальном проекте используется скрипт zad1.py.
В результате создается файл «eyeHandBlend.jpg», где глаз встраивается в руку.

eyeHandBlend.jpg

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

Первую часть я полностью перенес из оригинального проекта.

Код на питоне
    def colorTransfer(src, dst, mask):
    transferredDst = np.copy(dst)
    #indeksy nie czarnych pikseli maski
    maskIndices = np.where(mask != 0)
    #src[maskIndices[0], maskIndices[1]] zwraca piksele w nie czarnym obszarze maski

    maskedSrc = src[maskIndices[0], maskIndices[1]].astype(np.int32)
    maskedDst = dst[maskIndices[0], maskIndices[1]].astype(np.int32)

    meanSrc = np.mean(maskedSrc, axis=0)
    meanDst = np.mean(maskedDst, axis=0)

    maskedDst = maskedDst - meanDst
    maskedDst = maskedDst + meanSrc
    maskedDst = np.clip(maskedDst, 0, 255)

    transferredDst[maskIndices[0], maskIndices[1]] = maskedDst

    return transferredDst


Код перенесенный на C#
 static public Bitmap NewColor(Bitmap src, Bitmap ins, Rectangle r)
        {

            List<Vector> srV = new List<Vector>();
            List<Vector> inV = new List<Vector>(); ;


            for (int i = r.X; i < r.X + r.Width-2; i+=3)
            {
                for (int j = r.Y; j < r.Y + r.Height-3; j+=4)
                {
                    Color color = src.GetPixel(i, j);
                    Color color2 = ins.GetPixel(i, j);
                    srV.Add(new double[] { color.R, color.G, color.B }.ToVector());
                    inV.Add(new double[] { color2.R, color2.G, color2.B }.ToVector());
                }
            }

            Vector meanSrc = Vector.Mean(srV.ToArray()) / 255;
            Vector meanInk = Vector.Mean(inV.ToArray()) / 255;

            Tensor tensor = ImgConverter.BmpToTensor
            (ins.Clone(r, PixelFormat.Format32bppArgb));


            tensor = tensor.DivD(meanInk);
            tensor = tensor.PlusD(meanSrc);

            tensor = tensor.TransformTensor(x =>
            {
                if (x < 0) x = 0;
                if (x > 1) x = 1;
                return x;
            });

            return ImgConverter.TensorToBitmap(tensor);
        }


Чтобы края были более прозрачными, чем центральная часть изображения, для расчета альфа канала, была введена радиально-базисная функция следующего вида:
$\\k = 190 \\n = 3 \\r= (\frac{i-C_w}{1.2 \cdot W})^2+(\frac{j-C_h}{H})^2 \\ \alpha = 255 \cdot exp({-k\cdot r}^n)$

k и n были подобраны эмпирически.
i — индекс пикселя по оси OX
j — индекс пикселя по оси OY
$C_w$ — компонента x центра изображения
$C_h$ — компонента y центра изображения

В итоге я получил следующий результат:

eyeHandBlend.jpg

Поиск лица

Для поиска лица на фото существует множество алгоритмов:

  • Алгоритм Виолы-Джонса(каскады Хаара)
  • Hog+SVM
  • R-CNN
  • Fast R-CNN
  • Faster R-CNN
  • Yolo

Изначально использовался алгоритм Виолы-Джонса, но он оказался не достаточно точным, т.к. выделял лица не точно. Область выделения одного лица не совпадала с областью выделения второго, из-за чего замена происходила с дефектами, пример выделения лиц с помощь данного алгоритма показан ниже. Лица могут быть смещены, т.е. в на одном изображении оно захватывает оба уха, на другом только одно. Такие дефекты довольно плохо сказываются на конечном результате (на фото работа с DLib, предыдущая библиотека не всегда находила лицо, но к сожалению скриншоты не сохранились).



Далее я решил использовать Landmarks из библиотеки Dlib. Нашел DlibDotNet, который написан на .Net Core. Для использования в .Net Framework был создан промежуточный проект на .Net Standard 2.0 с основными функциями, поиска лица и выделения Landmarks.

Код на C#
public int[] Face(byte[] bts, int row, int col, int st)
{
       var img = Dlib.LoadImageData<RgbPixel>
       (ImagePixelFormat.Bgr, bts, (uint)row, (uint)col, (uint)st );
       var face = faceDetector.Operator(img)[0];
       int[] rect = { face.Left, face.Top, (int)face.Width, (int)face.Height};
       return rect;
}

public List<int[]> FacePoints(byte[] bts, int row, int col, int st)
 {
            List<int[]> points = new List<int[]>();
            var img = Dlib.LoadImageData<RgbPixel>
            (ImagePixelFormat.Bgr, bts, (uint)row, (uint)col, (uint)st);
            var face = faceDetector.Operator(img)[0];
            var shape = shapePredictor.Detect(img, face);
            for (var i = 0; i < shape.Parts; i++)
            {
                var point = shape.GetPart((uint)i);
                points.Add(new int[] { point.X, point.Y });
            }
            return points;
}


После чего написал библиотеку на .Net Framework 4.6.1, в которой реализовал всю логику.

Пример получения Langmarks:



Лицо можно выделить точнее, находя самую левую, правую, верхнюю и нижнюю точки и строя рамки по ним.



Потом лицо вырезалось из картинки в правом нижнем углу и вставлялось, с помощью описанного выше алгоритма, в картину: «Caballero de la mano en el pecho».

Был получен следующий результат.

image


В следующей статье я планирую рассмотреть создание 3D маски по фотографии.

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


  1. vladkorotnev
    07.10.2019 04:41

    Разрешите допоинтересоваться, в итоге цикл статей прийдёт к использованию нейросетей?


    Потому что в самом названии DeepFake подразумевается использование нейросети и методов глубокого обучения, а здесь пока что обычный face swap реализован


    1. Zachar_5 Автор
      07.10.2019 12:36
      -1

      Да, Вы правы, сейчас это FaceSwap. В первых 3х статьях я планирую повторить на языке C# этот проект.
      Первая нейронная сеть появится в 4-й части, там будет замена лица конкретного человека на видео. Нейросеть будет использоваться для распознавания лиц, до этого момента буду работать с одним лицом.
      В 5-й части планирую сделать "оживление" портрета. Т.е. синтезировать мимику по голосу, думаю использовать LSTM, на вход принимать спектрограмму голоса за короткий промежуток времени, на выходе выдавать изменения Landmarks.


      1. vladkorotnev
        07.10.2019 14:25
        +1

        Не знаю, кто влепил минус на коммент, но буду ждать, звучит интересно


      1. DeepFakescovery
        07.10.2019 17:57

        поясните а зачем вообще что-то повторять на C#?
        я сам фанат и C# и питона, но область дипфейков постоянно развивается, и если вы хотите развивать проект, то его нужно писать на питоне, потому что только на питоне есть обилие самых разных готовых библиотек с которыми легко быстро воплощать любую задумку, чего не скажешь про другие языки.


      1. ProX_Alex
        07.10.2019 22:18
        +1

        Теперь Вы мой кумир! Главное чтобы всё это воплотилось в жизнь. Побольше Вам свободного времени, чтобы эти запланированные статьи увидели свет!


        1. Zachar_5 Автор
          07.10.2019 22:19

          Спасибо!)


      1. subcommande
        09.10.2019 13:12
        +1

        Для этого я решил написать DeepFake на языке C#

        Да, Вы правы, сейчас это FaceSwap.


        1. Zachar_5 Автор
          09.10.2019 13:14

          Да, на данный момент это Face Swap. Но конечная цель — полноценный DeepFake.


  1. anshev0
    07.10.2019 17:09
    +1

    Спасибо автору. Жду продолжений с Neural Network на C#. Гуглил несколько дней назад примеры NN+GPU+C#, что-то не густо было. С интересом посмотрю, что выйдет.


    1. Zachar_5 Автор
      07.10.2019 17:19

      Кстати, что касается использования NN+GPU+C#, есть такие проекты:



      1. anshev0
        07.10.2019 17:25

        Спасибо! Посмотрю по ссылкам, что интересного.


    1. lostmsu
      10.10.2019 19:15

      Попиарюсь тоже: https://GitHub.com/losttech/Gradient-Samples


      Бесплатно для некоммерческого.


  1. Petr2009
    07.10.2019 22:19
    +1

    Мне кажется, что надо еще и фильтр для кожи использовать, типа что-нибудь вроде YUCIHighPassSkinSmoothing (https://github.com/YuAo/YUCIHighPassSkinSmoothing).