7 October 2019

DeepFake своими руками [часть 1]

.NETC#Machine learning
Не смотря на все прелести интернета, у него есть много минусов, и один из самых ужасных – это введения людей в заблуждение. Кликбейт, монтаж фотографий, ложные новости – все эти инструменты активно используются для обмана обычных пользователей в мировой сети, но в последние годы набирает обороты новый потенциально опасный инструмент, известный как 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 маски по фотографии.
Tags:deepfakeC#.net standard.net core.net framework 4.6.1dlibкомпьютерное зрение
Hubs: .NET C# Machine learning
+31
25.8k 153
Comments 16
Popular right now
.NET-разработчик
from 200,000 ₽Банк «Открытие»Москва
Разработчик .NET
from 60,000 ₽GMCSКазаньRemote job
.NET C# Software Engineer
from 3,000 to 4,000 $Hand2NoteRemote job
Ведущий программист .net
from 70,000 to 120,000 ₽Мечел-СервисЧелябинскRemote job
SharePoint Developer C# .NET
from 80,000 to 100,000 ₽Витро СофтRemote job
Top of the last 24 hours