Русский
Русский
English
Статистика
Реклама

Face recognition

Умное планирование рекламы в подкастах. Как собрать базу слушателей и распознать аудиторию с помощью нейросети

25.12.2020 16:21:43 | Автор: admin
Подкасты канал, который активно развивается весь 2020 год. Растет объем аудитории, да и самих подкастов становится все больше. При этом единого аудиторного измерителя слушателей не существует, да и вообще с измерениями этого канала дела обстоят не очень. При этом взаимный интерес подкастеров и рекламодателей довольно высокий.

Мы в dentsu придумали Podcaster аналитический инструмент для измерения аудитории подкастов и планирования рекламы в них. О том, как мы начали собирать данные и решили проблему распознавания аудитории, с какими трудностями столкнулись и что из этого вышло, рассказываем в этой статье.

image



Предыстория

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

  • подкасты можно выбрать из лимитированного списка, с которыми у продавца есть договоренности и есть данные по аудитории подкаста;
  • нет возможности выбрать более аффинитивные подкасты (аффинитивность отношение заданной ЦА среди слушателей ко всем слушателям подкаста), потому что, как правило, доступно описание ядра слушателей, а оно в целом одинаковое с точки зрения возраста для большинства подкастов;
  • данные по каждому подкасту есть у самих подкастеров, но как пересекаются слушатели между подкастами не знают ни сами подкастеры, ни продавцы.


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

Подход

Мы быстро поняли, что сами прослушивания с привязкой к пользователям получить мы не сможем. Но есть лайки/подписки на подкасты: подобная механика работает, например, в Instagram с блогерами, когда человек подписывается на блогера, чтобы видеть его новости. Мы предположили, что такая же история происходит и с подкастами слушатели подписываются на любимые подкасты, чтобы они были в быстром доступе и можно было следить за новыми выпусками.

Проверить эту гипотезу мы решили с помощью популярной платформы, через которую аудитория слушает подкасты. Согласно данным Tiburon, лидером по прослушиваниям подкастов является Яндекс.Музыка.

image

К счастью, на Я.Музыке есть страница пользователя, на которой представлена информация о подписках на подкасты.

Пример профиля с фото и подписками на подкасты

image

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

Механика

Мы начали собирать данные, а именно пользователей и подкасты, на которые слушатели подписаны. Изначально пользователей Я.Музыки с подкастами мы находили на данных сотрудников dentsu, которые предоставили свои почтовые ящики на Яндексе. Масштабировать проект не составило труда, так как мы не первый год работали с публичными данными.

Хорошая новость заключалась в том, что база подписанных на подкасты пользователей собиралась очень быстро буквально за полтора месяца мы набрали более 10 000 пользователей, которые подписаны хотя бы на один подкаст.

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

С данной задачей определения пола и возраста по фотографии отлично справилась наша нейросеть, точность которой составляет 96%. Алгоритм простой: берем фотографию пользователя Я.Музыки, ищем лицо и по нему определяем пол и возраст.

Лицо находится библиотекой face-recognition, использующей dlib. А в основе нашей нейросети лежит предобученная модель VGGFace на базе архитектуры ResNet-50, которую мы дообучали на фотографиях пользователей VK, доступных по публичному API. Датасет состоит из миллиона фотографий, которые дополнительно аугменировались через albumentations. Следует отметить, что для обучения мы не рассматриваем фотографии пользователей до 12 и после 65 лет.

Результаты

После обучения мы поняли, что примерно у 45% профилей пользователей с подкастами мы можем определить пол и возраст, так как много профилей без фото или картинкой, символом или просто с фотографией плохого качества. Но даже такой результат нас устраивает.

С учетом динамики нахождения профилей, которые подписываются на подкасты, мы ожидаем, что через несколько месяцев база слушателей составит 50 000 профилей, а у 22 500 из них мы будем знать пол и возраст.

Пример профиля, по которому мы не можем определить пол и возраст
image

Текущие наработки позволяют нам делать выборки аффинитивных подкастов для различных аудиторных групп.

Подборка подкастов на 20-50 в тематиках, которые релевантны бренду

image

Affinity = ЦА среди слушателей подкаста / все слушатели подкаста) / (все слушатели подкаста / все люди с подкастами)

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

Кривая построения охвата на 50 выбранных подкастах

image

Каждая точка +1 подкаст в микс. Первая точка подкаст с наибольшей уникальной аудиторией, последняя точка подкаст с наименьшей уникальной аудиторией.

Механика построения кривой и математическая модель

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

image

Далее мы вычеркиваем слушателей, которых охватываем подкастом 3, и опять выбираем тот подкаст, у которого уникальной аудитории больше всего (подкаст 4). Это подкаст, который дает нам 2 новых уникальных слушателя, поэтому следующим мы рекомендуем к размещению именно его.

image

Проделываем упражнение еще раз, и оказывается, что больше уникальных слушателей мы не охватим, то есть размещение в 2 подкастах из 6 достаточно, чтобы охватить всю возможную уникальную аудиторию.

image

Выводы

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

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

Автор Sasha_Kopylova
Подробнее..

Управляем звуком ПК от активности пользователя с помощью Python

17.06.2021 14:15:17 | Автор: admin

Настройка программного обеспечения

Без промедления начнём. Нам нужно установить следующее ПО:

  • Windows 10

  • Anaconda 3 (Python 3.8)

  • Visual Studio 2019 (Community) - объясню позже, зачем она понадобится.

Открываем Anaconda Prompt (Anaconda3) и устанавливаем следующие пакеты:

pip install opencv-pythonpip install dlibpip install face_recognition

И уже на этом моменте начнутся проблемы с dlib.

Решаем проблему с dlib

Я перепробовал все решения, что нашёл в интернете и они оказались неактуальными - раз, два, три, официальное руководство и видео есть. Поэтому будем собирать пакет вручную.

Итак, первая же ошибка говорит о том, что у нас не установлен cmake.

ERROR: CMake must be installed to build dlib
ERROR: CMake must be installed to build dlibERROR: CMake must be installed to build dlib

Не закрывая консоль, вводим следующую команду:

pip install cmake
Проблем при установке быть не должно

Пробуем установить пакет той же командой (pip install dlib), но на этот раз получаем новую ошибку:

Отсутствуют элементы Visual Studio

Ошибка явно указывает, что у меня, скорее всего, стоит студия с элементами только для C# - и она оказывается права. Открываем Visual Studio Installer, выбираем "Изменить", в вкладке "Рабочие нагрузки" в разделе "Классические и мобильные приложения" выбираем пункт "Разработка классических приложений на С++":

Пошагово
"Изменить""Изменить"Разработка классических приложений на С++Разработка классических приложений на С++Ждем окончания установкиЖдем окончания установки

Почему важно оставить все галочки, которые предлагает Visual Studio. У меня с интернетом плоховато, поэтому я решил не скачивать пакет SDK для Windows, на что получил следующую ошибку:

Не нашли компилятор

Я начал искать решение этой ошибки, пробовать менять тип компилятора (cmake -G " Visual Studio 16 2019"), но только стоило установить SDK, как все проблемы ушли.

Я пробовал данный метод на двух ПК и отмечу ещё пару подводных камней. Самое главное - Visual Studio должна быть 2019 года. У меня под рукой был офлайн установщик только 2017 - я мигом его поставил, делаю команду на установку пакета и получаю ошибку, что нужна свежая Microsoft Visual C++ версии 14.0. Вторая проблема была связана с тем, что даже установленная студия не могла скомпилировать проект. Помогла дополнительная установка Visual C++ 2015 Build Tools и Microsoft Build Tools 2015.

Открываем вновь Anaconda Prompt, используем ту же самую команду и ждём, когда соберется проект (около 5 минут):

Сборка
Всё прошло успешноВсё прошло успешно

Управляем громкостью

Вариантов оказалось несколько (ссылка), но чем проще - тем лучше. На русском язычном StackOverflow предложили использовать простую библиотеку от Paradoxis - ей и воспользуемся. Чтобы установить её, нам нужно скачать архив, пройти по пути C:\ProgramData\Anaconda3\Lib и перенести файлы keyboard.py, sound.py из архива. Проблем с использованием не возникало, поэтому идём дальше

Собираем события мыши

Самым популярным модулем для автоматизации управления мышью/клавиатурой оказался pynput. Устанавливаем так же через (pip install dlib). У модуля в целом неплохое описание - https://pynput.readthedocs.io/en/latest/mouse.html . Но у меня возникли сложности при получении событий. Я написал простую функцию:

from pynput import mousedef func_mouse():        with mouse.Events() as events:            for event in events:                if event == mouse.Events.Scroll or mouse.Events.Click:                    #print('Переместил мышку/нажал кнопку/скролл колесиком: {}\n'.format(event))                    print('Делаю половину громкости: ', time.ctime())                    Sound.volume_set(volum_half)                    break

Самое интересное, что если раскомментировать самую первую строчку и посмотреть на событие, которое привело выходу из цикла, то там можно увидеть Move. Если вы заметили, в условии if про него не слово. Без разницы, делал я только скролл колесиком или только нажатие любой клавиши мыши - все равно просто движение мыши приводит к выходу из цикла. В целом, мне нужно все действия (Scroll, Click, Move), но такое поведение я объяснить не могу. Возможно я где-то ошибаюсь, поэтому можете поправить.

А что в итоге?

Adam Geitgey, автор библиотеки face recognition, в своём репозитории имеет очень хороший набор примеров, которые многие используют при написании статей: https://github.com/ageitgey/face_recognition/tree/master/examples

Воспользуемся одним из них и получим следующий код, который можно скачать по ссылке: Activity.ipynb, Activity.py

Код
# Подключаем нужные библиотекиimport cv2import face_recognition # Получаем данные с устройства (веб камера у меня всего одна, поэтому в аргументах 0)video_capture = cv2.VideoCapture(0) # Инициализируем переменныеface_locations = []from sound import SoundSound.volume_up() # увеличим громкость на 2 единицыcurrent = Sound.current_volume() # текущая громкость, если кому-то нужноvolum_half=50  # 50% громкостьvolum_full=100 # 100% громкостьSound.volume_max() # выставляем сразу по максимуму# Работа со временем# Подключаем модуль для работы со временемimport time# Подключаем потокиfrom threading import Threadimport threading# Функция для работы с активностью мышиfrom pynput import mousedef func_mouse():        with mouse.Events() as events:            for event in events:                if event == mouse.Events.Scroll or mouse.Events.Click:                    #print('Переместил мышку/нажал кнопку/скролл колесиком: {}\n'.format(event))                    print('Делаю половину громкости: ', time.ctime())                    Sound.volume_set(volum_half)                    break# Делаем отдельную функцию с напоминаниемdef not_find():    #print("Cкрипт на 15 секунд начинается ", time.ctime())    print('Делаю 100% громкости: ', time.ctime())    #Sound.volume_set(volum_full)    Sound.volume_max()        # Секунды на выполнение    #local_time = 15    # Ждём нужное количество секунд, цикл в это время ничего не делает    #time.sleep(local_time)        # Вызываю функцию поиска действий по мышке    func_mouse()    #print("Cкрипт на 15 сек прошел")# А тут уже основная часть кодаwhile True:    ret, frame = video_capture.read()        '''    # Resize frame of video to 1/2 size for faster face recognition processing    small_frame = cv2.resize(frame, (0, 0), fx=0.50, fy=0.50)    rgb_frame = small_frame[:, :, ::-1]    '''    rgb_frame = frame[:, :, ::-1]        face_locations = face_recognition.face_locations(rgb_frame)        number_of_face = len(face_locations)        '''    #print("Я нашел {} лицо(лица) в данном окне".format(number_of_face))    #print("Я нашел {} лицо(лица) в данном окне".format(len(face_locations)))    '''        if number_of_face < 1:        print("Я не нашел лицо/лица в данном окне, начинаю работу:", time.ctime())        '''        th = Thread(target=not_find, args=()) # Создаём новый поток        th.start() # И запускаем его        # Пока работает поток, выведем на экран через 10 секунд, что основной цикл в работе        '''        #time.sleep(5)        print("Поток мыши заработал в основном цикле: ", time.ctime())                #thread = threading.Timer(60, not_find)        #thread.start()                not_find()        '''        thread = threading.Timer(60, func_mouse)        thread.start()        print("Поток мыши заработал.\n")        # Пока работает поток, выведем на экран через 10 секунд, что основной цикл в работе        '''        #time.sleep(10)        print("Пока поток работает, основной цикл поиска лица в работе.\n")    else:        #все хорошо, за ПК кто-то есть        print("Я нашел лицо/лица в данном окне в", time.ctime())        Sound.volume_set(volum_half)            for top, right, bottom, left in face_locations:        cv2.rectangle(frame, (left, top), (right, bottom), (0, 0, 255), 2)        cv2.imshow('Video', frame)        if cv2.waitKey(1) & 0xFF == ord('q'):        breakvideo_capture.release()cv2.destroyAllWindows()

Суть кода предельно проста: бегаем в цикле, как только появилось хотя бы одно лицо (а точнее координаты), то звук делаем 50%. Если не нашёл никого поблизости, то запускаем цикл с мышкой.

Тестирование в бою

Ожидание и реальность

Если вы посмотрели видео, то поняли, что результат ещё далёк от реальной эксплуатации.

Признаю честно - до этого момента никогда не сталкивался с многопоточностью на Python, поэтому "с наскоку" тему взять не удалось и результат по видео понятен. Есть неплохая статья на Хабре, описывающая различные методы многопоточности, применяемые в языке. Пока у меня решения нету по этой теме нету - будет повод разобраться лучше и дописать код/статью с учетом этого.

Так же возникает закономерный вопрос - а если вместо живого человека поставить перед монитором картинку? Да, она распознает, что, скорее всего, не совсем верно. Мне попался очень хороший материал по поводу определения живого лица в реальном времени - https://www.machinelearningmastery.ru/real-time-face-liveness-detection-with-python-keras-and-opencv-c35dc70dafd3/ , но это уже немного другой уровень и думаю новичкам это будет посложнее. Но эксперименты с нейронными сетями я чуть позже повторю, чтобы тоже проверить верность и повторяемость данного руководства.

Немаловажным фактором на качество распознавания оказывает получаемое изображение с веб-камеры. Предложение использовать 1/4 изображения (сжатие его) приводит только к ухудшению - моё лицо алгоритм распознать так и не смог. Для повышения качества предлагают использовать MTCNN face detector (пример использования), либо что-нибудь посложнее из абзаца выше.

Другая интересная особенность - таймеры в Питоне. Я, опять же, признаю, что ни разу до этого не было нужды в них, но все статьях сводится к тому, чтобы ставить поток в sleep(кол-во секунд). А если мне нужно сделать так, чтобы основной поток был в работе, а по истечению n-ое количества секунд не было активности, то выполнялась моя функция? Использовать демонов (daemon)? Так это не совсем то, что нужно. Писать отдельную программу, которая взаимодействует с другой? Возможно, но единство программы пропадает.

Заключение

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

P.S. Предлагаю вам, читатели, обсудить в комментариях статью - ваши идеи, замечания, уточнения.

Подробнее..

Распознавание Ворониных на фотографиях от концепции к делу

17.01.2021 16:22:22 | Автор: admin

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

Откуда родилась задача

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

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

Что будем использовать

Первым шагом к решению проблемы стал выбор инструментария. После изучения нескольких популярных решений и подходов, выбор пал на библиотеку face-recognition. О других реализациях распознавания нестандартных объектов постараюсь рассказать в следующих статьях (дайте знать, если тема распознавания грибов на фото трогает вашу душу).

Выбранная библиотека имеет довольно широкий функционал по распознаванию лиц на фотографиях, а также довольно приятную документацию и ряд интересных примеров с фрагментами кода. Помимо фото, она позволяет распознавать лица и на видео (не без помощи openCV).

Устанавливается face-recognition довольно легко, единственная важная оговорка - пользователям Mac и Linux (в моем случае была Ubuntu 20.04) необходимо вручную установить dlib. Процесс тоже довольно подробно описан в документации.

Немного о том, как все работает

face-regontion использует внутри себя обозначенный выше dlib и OpenFace. Сначала на фотографии выделяется лицо, а после на лице выделяется 68 значимых точек.

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

Подробно о том, как все устроено, можно узнать в оригинальном посте автора библиотеки.

От слов к реализации

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

На серверной стороне python с fast api. Эта давно зарекомендовавшая себя пара и тут продемонстрировала все свои красоту и удобство.

Первым делом, нам необходим набор из фото, который позволяет заранее построить кодировки лиц героев. И тут раскрывается очередной и довольно весомый плюс face-recognition - нам нужно всего одно фото, по которому мы можем построить математическую проекцию лица. Нам нет нужды собирать большой датасет из тысяч или даже десятков тысяч изображений.

Далее мы загружаем в систему все наши тестовые фотографии, чтобы выделить кодировки лиц:

kostik = face_recognition.load_image_file("samples/kostik.jpg")kostik_encoding = face_recognition.face_encodings(kostik)[0]vera = face_recognition.load_image_file("samples/vera.jpg")vera_encoding = face_recognition.face_encodings(vera)[0]

После я реализовал набор функций, которые принимают на вход кодировку присланного изображения, сравнивают его с определенном персонажем, а после возвращают либо имя, либо вызов следующей функции. Выглядит все следующим образом:

def start_comparing(encoding):compare_result = face_recognition.compare_faces([kostik_encoding], encoding)if compare_result[0]:return "Костик"else:return compare_vera(encoding)def compare_vera(encoding):compare_result = face_recognition.compare_faces([vera_encoding], encoding)if compare_result[0]:return "Вера"else:return compare_nikolay(encoding)

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

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

Ну и в конце нужно реализовать обычный API-поинт, который умел бы принимать фото и возвращать результат:

@app.post("/recognize/")def create_file(file: UploadFile = File(...)):with open("samples/test.jpeg", "wb") as buffer:    shutil.copyfileobj(file.file, buffer)unknown_image = face_recognition.load_image_file("samples/test.jpeg")try:unknown_encoding = face_recognition.face_encodings(unknown_image)[0]result = start_comparing(unknown_encoding)except Exception as e:result = "Тут нет лица"else:passfinally:passreturn {"filename": file.filename, "result": result}

Получаем файл изображения, сохраняем его, кодируем и запускаем алгоритм сравнения. Если вы все сделали правильно, не забыли про CORS и написали веб-приложение (или использовали мое, с 2 формами), то теперь у вас готовая система, которая умеет отличать Галю от Лени. Итоговое решение можно найти тут. Если вдруг кто-то заинтересуется и будет скучать томными вечерами, то присылайте пулл-реквесты - буду рад!

Выводы

Теперь немного выводов:

  • Искусственный интеллект и распознавание изображений в частности - это очень сложный домен, который требует большого количество знаний и опыта. Однако существует ряд инструментов, которые помогут облегчить вам жизнь и внедрить необычные технологии "малой кровью".

  • Библиотека замечательно показала себя в учебном проекте. Использование ее будет вполне оправдано в хакатонах или же стартапах, где нужно как можно быстрее реализовать MVP.

  • Не стоит бездумно использовать библиотеку в больших проектах, все же лучше изучить ограничения производительности.

  • Навык импорта библиотеки в python и последующее использование ее методов не делает из программиста Senior Data Sciene Engineer. Область искусственного интеллекта куда как сложнее и глубже.

  • Математику все же забывать не стоит :)

Подробнее..

Ещё один поиск Вк по фото

20.03.2021 16:14:02 | Автор: admin

Я люблю ввязываться в авантюры, и за последний месяц об одной из них я пару раз рассказывал друзьям, что вызывало восторг, поэтому решил поделиться с хабравчанами! Эта история про отважные пет-проекты, мощь опен-сорса и саморазвитие, а также основные технические детали. Надеюсь, вас это вдохновит :)

1. Предыстория

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

В то время мне об этом сервисе говорили и ленты новостей, и друзья, я отвечал "ну да, прикольно", и только. Но спустя пару лет, в начале октябре 2018 на каком-то айтишном форуме я захотел связаться с одним пользователем по специфическому вопросу, вот только он туда уже давно не заходил. Зато там было его хорошее фото, и тут-то я вспомнил про крутой сервис! Побежал на их сайт и разочаровался в сентябре 2018, буквально за месяц, они перестали предоставлять свои услуги физ.лицам, и бесплатно, и даже за деньги, перейдя в сегмент b2b и b2g. Оно и понятно, пиар уже сработал, а этических вопросов так возникает куда меньше. Но меня, законопослушного гражданина, это огорчило. И не только меня: фан-группы ФайндФейса пестрили сообщениями о том, что люди готовы заплатить в 10 раз больше, лишь бы им помогли найти нужного человека.

Я стал искать аналоги, но они все были либо точным поиском, но по крайне ограниченной выборке вроде нескольких сотен знаменитостей из Википедии, либо обширным, но почти бесполезным по точности поиском через Гугл/Яндекс.

Пару рабочих дней я размышлял, что же сложного в создании такого сервиса, который бы и точно искал людей, и по всему Вк? Решил, что ничего, ведь у меня тогда уже были базовые познания в data science, разработке и администрировании. Поэтому в пятницу, приехав с работы домой, я взялся за дело. За вечер я накидал скрипт, который парсит профили Вк, находит фото, индексирует по ним лица и сохраняет в БД. Потом просидел ещё пару суток почти без сна, заставил это дело безостановочно работать на своём сервере. Началась новая трудовая неделя, я был очень уставший, но ещё больше довольный и полный энтузиазма! Ведь мой скрипт медленно, но безостановочно бежал по всему Вк.

2. Техническое устройство

2.1. Индексирование

Как вы считаете, что происходит после того, как вы отправляете запрос в любую крупную поисковую систему? Не важно, поиск текста в Яндексе, Google или поиск лиц в FindFace или моём сервисе. Многие, особенно не-айтишники, с трудном представляют внутренние механики технических процессов, а они бывают нетривиальны даже казалось бы в простых задачах. В случае поисковых систем магия заключается в том, что при получении запроса они не начинают обегать все страницы в интернете, ища там ваш текст, или весь Вк, сравнивая вашу фотку со всеми подряд, это бы занимало астрономические объёмы времени. Вместо этого, поисковые системы сперва индексируют нужные данные. В случае текста (и подобных тексту данных вроде ДНК) в ближайшем приближении могут использоваться хэш-таблицы или префиксные деревья. В случае фоток тоже нужны индексы, которые сильно сократят время поиска. Для этого я использовал библиотеку face_recognition, которая позволяет преобразовать фото лица, если правильно помню, в 128-мерный вектор признаков со значениями от -1 до 1 (далее буду называть его просто хэш). Для поиска человека по фото, нам нужно просто пробежаться по всем фото из коллекции, считая евклидово расстояние между векторами-хэшами из запроса и набора подобный пример, реализованный на Питоне, доступен на сайте упомянутой библиотеки. Да, такая операция поиска тоже не дешёвая, но об этом позже.

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

Конечно, не только лимиты АПИ повышать надо, но и объёмы CPU. Изначально я развернул скрипт на маленьком VPS, который создавался для простого личного сайта. В подмогу ему, я взял ещё один VPS, в несколько раз мощнее. Потом я решил, что и этого мало, взял ещё и целый выделенный сервер, который сильнее моего собственного рабочего компьютера :D Не энтерпрайз-левел, но производительность стала меня устраивать, хотя расходы и выросли до 15 тысяч руб/месяц, что для меня тогда было весьма ощутимой тратой.

2.2. Подобие архитектуры и DevOps'а

Если у вас есть опыт в бэкэнд разработке, то сразу встаёт вопрос, как я заставил несколько серверов работать в тандеме, а не независимо, дублируя одну и ту же работу? На самом деле, решение классическое: один микросервис брал на себя роль ведущего, мастера, хранил состояние всей системы, и выдавал задания объёмом в тысячу профилей Вк воркерам, которые быстрее или медленнее их индексировали, а результаты, в том числе промежуточные, возвращали мастеру; если же мастер за определённый промежуток времени не получал ответа от воркера, он давал это задание другому, с учётом промежуточного прогресса.

Кстати, воркеры работали в несколько потоков. Да, Питон, благодаря Global Interpreter Lock, не умеет в полный параллелизм, но много времени уходило на выгрузку фоток, а IO-операции хорошо параллелятся. Вдобавок, это позволило легко назначить каждому потоку свой токен доступа и гибко настраивать загруженность каждой машины.

Для автоматизации настройки окружения, токенов и т.п были написаны скрипты на Питоне, которые подключались к целевой машине по SSH и ставили всё что нужно. Позже я узнал, что у меня костыльный велосипед, есть качественные решения, но всё равно было интересно посмотреть подноготные детали. Из прикольного, пришлось также разобраться, что есть разные ВМ и средства виртуализации, что некоторое ПО не работает в определённых конфигурациях, благодаря чему виртуалки на Xen и OpenVZ с казалось бы одинаковыми ресурсами могут отличаться в цене на 40%.

2.3. Поиск

Помимо ролей мастера и воркера, есть роль поискового микросервиса. Проиндексированные фото Вк и айдишники их профилей сохраняются в БД, точнее, MySQL v5.7 и алгоритм поиска я переписал с Python на SQL, что позволило сильно ускорить вычисления и выйти на больший масштаб. Но с ростом данных этого всё равно было очень мало, я думал над оптимизациями, старался переиспользовать свой опыт big data аналитики с работы, экспериментировал с разными структурами запросов и генерацией SQL-запросов Питоном, это позволило ускорить вычисления в несколько раз, что мило, но всё равно мало.

Потом я решил сделать поиск двух-этапным: преобразовывать хэши-дробные-векторы в небольшой массив байт, сохраняя каждый признак в два бита: v>0.1 и v<-0.1 (здесь), затем сравнивая число совпавших бит такого хэша у целевого лица и всех лиц в БД, а потом фильтруя записи в БД по какому-то трешхолду, отправляя на более точное и медленное сравнение только потенциальных кандидатов. Пришлось повозиться и переехать на MySQL v8, т.к в 5.7 бинарных операций нет. Но это позволило ускорить поиск ещё почти в 30 раз а это уже клёво ^_^

Вообще, поиск можно было бы улучшать и дальше возможно подобрав более эффективные коэффициенты в операции выше, или переделав этот хэш, ещё было бы круто добавить параллелизм, которого в MySQL из коробки нет, партицирование, шардирование, и искать многопоточно; для этого в планах был переезд на PostgreSQL.

2.4. Другие механики

Когда у меня накопился большой объём данных, и мой сервис научился находить некоторую долю людей по случайным фоткам за приемлемое время, я стал мечтать дальше и задумываться о развитии функционала.

Ускорять время поиска можно не только ускорением самого поискового алгоритма, но и снижением выборки, например, ища профили только среди участников каких-то групп Вк. Ясное дело, здесь встаёт вопрос ограничения размера этой выборки, т.к делать запрос с "ISIN (десятки тысяч айдишников)" такое себе, а вот на паре сотен и даже тысяч работает в разы быстрее, чем полный проход БД.

Помимо прочего, я немного помешан на математике, множествах графах, а соц.сети это прекрасные данные и множеств, и графов! Я подумал, что можно двигаться в этом направлении, позволяя задавать сложные запросы с И-ИЛИ-деревьями. Притом, искать можно не только по фото, но и по ник-неймам или ссылкам на профили в других соц.сетях к сожалению, поисковики интернета и Вк эту инфу не очень индексируют, и я попробовал делать это сам.

Если идти ещё дальше, то можно индексировать не только Вк, но и ВотсАп, Тг перебрав все русские номера, возможно частично FB, Twi, Ig. Но это уже совсем будущее, я решил двигаться в сторону скорейшей апробации и монетизации того, что есть уже.

3. Заключение

3.2. Happy ли end?

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

Я написал в тех поддержку Вк (тогда они ещё отвечали, ахах), аккуратно представился студентом, что хочу проводить социологические исследования скандируя большие объёмы данных Вк, в т.ч фото, ФИО и описание. Что на самом деле было правдой, с учётом моего интереса к аналитике и психологии. Они ответили, что ради статистики и небольших выборок в целом не против, но точно против какой-либо идентификации. А ещё "порадовали" тем, что будут и палки в колёса АПИ вставлять таким сервисам, и участвовать в разработке/внедрению законов, регулирующих эту деятельность. А недавно, уже в наше время, вышел законопроект, запрещающий автоматизированную обработку данных с сайтов, что по сути полностью блокирует подобные сервисы с парсингом.

В связи с этим, я принял решение о закрытии проекта, хоть это и было печально: в феврале 2019 у меня уже было проиндексировано 25% всего Вк в гигабайтах БД, притом не за бесплатно. Но у меня уже тогда был опыт различных проектов, поэтому я не жил розовыми мечтами об успешном успехе, а старался извлечь другую пользу и просто фан (:

Кстати, только в процессе написания этой статьи я понял, что принявшись за эту работу, я совсем забыл про исходный мотив, про того человека с какого-то сайта и свой вопрос к нему xD

После завершения описанной истории, я решил опубликовать исходники, но т.к там в истории коммитов засветились токены, то перезалил в новый репозиторий. Но код действительно такой, что мне самому туда страшно заглядывать :D
https://github.com/AivanF/ID-Detective-public

3.2. Польза

Здесь, как и в других своих пет-проектах и стартапах, я набрался много опыта:

  • Разобрался с многопоточностью в Питоне.

  • Покопался в специфических вопросах оптимизации MySQL запросов.

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

  • Освоил работу из кода с SSH для настройки окружения, понял, насколько чудесен Ansible.

  • Разработал микросервисную архитектуру из клея и палок, что затем позволило легко понять концепции Kubernetes.

И всё это мне очень пригодилось в последующих работах и проектах.

3.3. Мораль

Выводы каждый сделает свои, но главное не бойтесь пробовать, учиться и искать себя! Надеюсь, вам было интересно :)

Подробнее..

Android и 3D камера. Распознавание лиц с защитой от Fraud

25.06.2020 18:18:39 | Автор: admin
Привет! Меня зовут Владимир Шальков, я Android-разработчик в Surf.

Не так давно нам необходимо было реализовать систему распознавания лиц на Android с защитой от мошенничества (fraud). В этой статье я поделюсь самыми интересными аспектами реализации с примерами кода и ссылками. Уверен, вы найдёте что-то новое и интересное для себя, поэтому усаживайтесь поудобнее, начинаем.



Системы распознавания лиц сейчас становятся всё более и более востребованными: количество устройств с функцией разблокировки по лицу растёт, так же как и количество инструментов для разработчиков.

Компания Apple в своих продуктах использует FaceID, кроме этого они позаботились о разработчиках и добавили API для доступа к этой функциональности. FaceID считается достаточно безопасным и его можно использовать для разблокировки банковских приложений. Android SDK же до недавнего времени не имел готового решения. Хотя производители устройств добавляли в свои прошивки возможность разблокировать устройство с помощью лица, разработчики не могли использовать функциональность в приложениях, да и безопасность такого способа разблокировки, оставляла желать лучшего.

Недавно, класс FingerprintManager, который использовался для разблокировки приложений по отпечатку пальцев, задепрекейтили на API 28 и выше, и разработчикам предлагается использовать BiometricPrompt. Это класс, содержит логику, связанную с биометрией, в том числе по идентификации лиц. Однако использовать его в каждом смартфоне не получится, потому что согласно информации от Google, устройство должно иметь высокий рейтинг безопасности.

Некоторые устройства не имеют встроенного сканера отпечатка пальцев, от него отказались ввиду высокого уровня защиты от мошенничества при распознавании лица и всё благодаря фронтальному ToF(Time-of-flight) датчику. С его помощью можно построить карту глубины, тем самым увеличить устойчивость системы к взлому.

Требования


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

Основной целью мы ставили обеспечение максимального уровня безопасности: необходимо было минимизировать возможность обхода системы распознавания лиц, например, с помощью фотографии, которую поднесли к видоискателю. Для этого решили использовать 3D-камеру Intel RealSense (модель D435i), которая имеет встроенный ToF датчик, благодаря ему можно получить все необходимые данные для построения карты глубины.

image

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

Ещё одно не менее важное ограничение работа в оффлайн режиме. Из-за этого мы не могли применять облачные сервисы для распознавания лиц. Кроме этого писать алгоритмы распознавания лиц с нуля неразумно, с учётом ограничения времени и трудозатрат. Возникает вопрос: зачем изобретать велосипед, если уже есть готовые решения? Исходя из всего выше сказанного, решили использовать библиотеку Face SDK от 3DiVi.

Получение изображения с камеры Intel RealSense


На первом этапе реализации необходимо было получить два изображения с 3D камеры: одно цветное, второе с картой глубины. Потом они будут использоваться библиотекой Face SDK для дальнейших вычислений.

Чтобы начать работать с камерой Intel RealSense в Android-проекте, необходимо добавить зависимость RealSense SDK for Android OS: она является оберткой над официальной C++ библиотекой. В официальных семплах можно найти как произвести инициализацию и отобразить картинку с камер, на этом останавливаться не будем, там всё достаточно просто. Перейдём сразу к коду получения изображений:

private val pipeline = Pipeline()private val streamingHandler = Handler()private var streamRunnable: Runnable = object : Runnable {    override fun run() {        try {            FrameReleaser().use { fr ->                val frames = pipeline.waitForFrames(1000).releaseWith(fr)                val orgFrameSet = frames.releaseWith(fr)                val processedFrameSet = frames.applyFilter(align).releaseWith(fr)                val orgFrame: Frame = orgFrameSet.first(StreamType.COLOR, StreamFormat.RGB8).releaseWith(fr)                // Получаем фрейм цветного изображения                val videoFrame: VideoFrame = orgFrame.`as`(Extension.VIDEO_FRAME)                val processedDepth: Frame = processedFrameSet.first(StreamType.DEPTH, StreamFormat.Z16).releaseWith(fr)                // Получаем фрейм глубины изображения                val depthFrame: DepthFrame = processedDepth.`as`(Extension.DEPTH_FRAME)                upload(orgFrame) // Выводим на экран цветное изображение            }            streamingHandler.post(this)        } catch (e: Exception) {            Logger.d("Streaming, error: " + e.message)        }    }}streamingHandler.post(streamRunnable) // Запуск

С помощью FrameReleaser() мы получаем отдельные кадры с видеопотока, которые имеют тип Frame. К фреймам можно применять различные фильтры через applyFilter().

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

Если мы хотим отобразить картинку на экране устройства, то для этого существует метод upload(), в параметрах указывается тип фрейма, который нужно отобразить на экране, у нас это кадры с цветной камеры.

Преобразование фреймов в изображение


На следующем этапе необходимо получить из VideoFrame и DepthFrame картинки в нужном нам формате. Эти картинки мы будем использовать, чтобы определять принадлежит ли лицо на изображении реальному человеку и добавлять информацию в базу данных.

Формат изображений:

  • Цветное, с расширением .bmp, получаемое из VideoFrame
  • С картой глубины, имеющее расширение .tiff и получаемое из DepthFrame

Чтобы осуществить преобразования нам понадобится библиотека компьютерного зрения с открытым исходным кодом OpenCV. Вся работа заключается в формировании объекта Mat и конвертировании его в нужный формат:

fun videoFrameToMat(videoFrame: VideoFrame): Mat {    val colorMat = Mat(videoFrame.height, videoFrame.width, CvType.CV_8UC3)    val returnBuff = ByteArray(videoFrame.dataSize)    videoFrame.getData(returnBuff)    colorMat.put(0, 0, returnBuff)    val colorMatNew = Mat()    Imgproc.cvtColor(colorMat, colorMatNew, Imgproc.COLOR_RGB2BGR)    return colorMatNew}

Для сохранения цветного изображения необходимо сформировать матрицу с типом CvType.CV_8UC3, после конвертировать в BRG, чтобы цвета имели нормальный оттенок.
Используя метод Imgcodecs.imwrite, сохранить на устройстве:

fun VideoFrame.saveToFile(path: String): Boolean {    val colorMat = videoFrameToMat(this)    return Imgcodecs.imwrite(path + COLOR_IMAGE_FORMAT, colorMat)}

Тоже самое необходимо проделать для DepthFrame с тем лишь отличием, что матрица должна быть с типом CvType.CV_16UC1, так как изображение будет строиться из кадра, который содержит данные с датчика глубины:

fun depthFrameToMat(depthFrame: DepthFrame): Mat {    val depthMat = Mat(depthFrame.height, depthFrame.width, CvType.CV_16UC1)    val size = (depthMat.total() * depthMat.elemSize()).toInt()    val returnBuff = ByteArray(size)    depthFrame.getData(returnBuff)    val shorts = ShortArray(size / 2)    ByteBuffer.wrap(returnBuff).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(shorts)    depthMat.put(0, 0, shorts)    return depthMat}

Сохранение изображения с картой глубины:

fun DepthFrame.saveToFile(path: String): Boolean {    val depthMat = depthFrameToMat(this)    return Imgcodecs.imwrite(path + DEPTH_IMAGE_FORMAT, depthMat)}

Работа с библиотекой Face SDK


Face SDK имеет большой объём программных компонентов, но большая часть из них нам не нужна. Библиотека так же, как и RealSense SDK написана на C++ и имеет обёртку, чтобы было удобно работать под Android. Face SDK не бесплатна, но если вы разработчик, то вам выдадут тестовую лицензию.

Большинство компонентов библиотеки настраиваются с помощью XML конфигурационных файлов. В зависимости от конфигурации, будет применяться тот или иной алгоритм.
Чтобы начать работать необходимо создать экземпляр класса FacerecService, он используется при инициализации других компонентов, в параметрах передается путь до DLL библиотек, конфигурационных файлов и лицензии.

Далее, используя этот сервис, нужно создать объекты классов FacerecService.Config и Capturer:

private val service: FacerecService = FacerecService.createService(                dllPath,                confDirPath,                onlineLicenseDir        )private val confManual: FacerecService.Config = service.Config("manual_capturer.xml")private val capturerManual: Capturer = service.createCapturer(confManual)

Класс Capturer используется для распознавания лиц. Конфигурация manual_capturer.xml означает, что мы будем использовать алгоритмы из библиотеки OpenCV это детектор фронтальных лиц Viola-Jones, для распознавания используются признаки Хаара. Библиотека предоставляет готовое множество XML файлов с конфигурациями, отличающихся по характеристикам качества распознавания и времени работы. Менее быстрые методы имеют лучшие показатели по качеству распознавания. Если нам нужно распознавать лица в профиль, то следует использовать другой конфигурационный XML файл common_lprofile_capturer.xml. Конфигов достаточно много, с ними можно подробнее ознакомиться в документации. В нашем случае необходимо было использовать конфиг common_capturer4_singleface.xml это конфигурация с пониженным порогом качества в результате использования которой, всегда будет возвращаться не более одного лица.

Чтобы найти лицо на изображении применяется метод capturerSingleFace.capture(), в который передаётся массив байтов картинки, которая содержит лицо человека:

fun createRawSample(imagePath: String): RawSample? {    val imageColorFile = File(imagePath)    val originalColorByteArray = ImageUtil.readImage(imageColorFile)    return capturerSingleFace.capture(originalColorByteArray).getOrNull(0)}

Объект RawSample хранит информацию о найденном лице и содержит набор различных методов, например если вызвать getLandmarks(), то можно получить антропометрические точки лица.

Принадлежность лица реальному человеку


Чтобы определить реальный ли человек находится в кадре, а не фотография, приставленная к камере детекции лиц, библиотека Face SDK, предоставляет модуль DepthLivenessEstimator, он возвращает enum с одним из четырех значений:

  • NOT_ENOUGH_DATA слишком много отсутствующих значений на карте глубины
  • REAL наблюдаемое лицо принадлежит живому человеку
  • FAKE наблюдаемое лицо является фотографией
  • NOT_COMPUTED не удалось произвести вычисления

Инициализация модуля:

val depthLivenessEstimator: DepthLivenessEstimator = service.createDepthLivenessEstimator(           "depth_liveness_estimator_cnn.xml"   )

Определение принадлежности лица реальному человеку:

fun getLivenessState(            rgbPath: String,            depthPath: String    ): DepthLivenessEstimator.Liveness {        val imageColorFile = File(rgbPath + COLOR_IMAGE_FORMAT)        val originalColorByteArray = readImage(imageColorFile)        val originalRawSimple = capturerSingleFace.capture(originalColorByteArray).getOrNull(0)        val originalRawImage = RawImage(                SCREEN_RESOLUTION_WIDTH,                SCREEN_RESOLUTION_HEIGHT,                RawImage.Format.FORMAT_BGR,                originalColorByteArray        )        val originalDepthPtr = Natives().readDepthMap(depthPath + DEPTH_IMAGE_FORMAT)// параметры камеры        val hFov = 69.4f         val vFov = 42.5f         val depthMapRaw = DepthMapRaw()        with(depthMapRaw) {            depth_map_rows = originalRawImage.height            depth_map_cols = originalRawImage.width            depth_map_2_image_offset_x = 0f            depth_map_2_image_offset_y = 0f            depth_map_2_image_scale_x = 1f            depth_map_2_image_scale_y = 1f            horizontal_fov = hFov            vertical_fov = vFov            depth_unit_in_millimeters = 1f            depth_data_ptr = originalDepthPtr            depth_data_stride_in_bytes = (2 * originalRawImage.width)        }        return depthLivenessEstimator.estimateLiveness(originalRawSimple, depthMapRaw)}

Метод getLivenessState() в качестве параметров получает ссылки на изображения: цветное и с картой глубины. Из цветного мы формируем объект RawImage, этот класс предоставляет данные изображения в сыром виде и опциональной информации для обрезки. Из карты глубины формируется DepthMapRaw карта глубины, отрегистрированная в соответствии с исходным цветным изображением. Это необходимо сделать, чтобы использовать метод estimateLiveness(originalRawSimple, depthMapRaw), который вернёт нам enum с информацией реальное ли лицо было в кадре.

Стоит обратить внимание на формирование объекта DepthMapRaw. Одна из переменных имеет наименование depth_data_ptr это указатель на данные глубины, но как известно в Java нет указателей. Для получения указателя надо воспользоваться JNI функцией, которая в качестве аргумента принимает ссылку на изображение с картой глубины:

extern "C" JNIEXPORT jlong JNICALL Java_ru_face_detect_Natives_readDepthMap(JNIEnv *env, jobject obj, jstring jfilename){    const char * buf = env->GetStringUTFChars(jfilename, NULL);    std::string filename = buf;    env->ReleaseStringUTFChars(jfilename, buf);    cv::Mat depth_map = cv::imread(filename, -1);    unsigned char * data = new unsigned char[depth_map.rows * depth_map.cols * depth_map.elemSize()];    memcpy(data, depth_map.data, depth_map.rows * depth_map.cols * depth_map.elemSize());    return (jlong) data;}

Для вызова кода написанного на C в Kotlin, необходимо создать класс такого типа:

class Natives {    init {        System.loadLibrary("native-lib")    }    external fun readDepthMap(fileName: String): Long}

В System.loadLibrary() передаётся наименование файла .cpp, где содержится метод readDepthMap(), в нашем случае это native-lib.cpp. Также необходимо поставить модификатор external, который означает, что метод реализован не в Kotlin.

Идентификация лица


Не менее важная функция определение личности найденного лица в кадре. Face SDK позволяет реализовать это с помощью модуля Recognizer. Инициализация:

val recognizer: Recognizer = service.createRecognizer(        "method8v7_recognizer.xml",        true,        true,        true)

Мы используем конфигурационный файл method8v7_recognizer.xml, который имеет самую высокую скорость распознавания, но при этом качество распознавания ниже, чем у методов 6v7 и 7v7.

Перед тем, как идентифицировать лицо, необходимо создать список лиц, используя который мы будем находить соответствие по образцу фотографии. Для реализации, нужно создать Vector из объектов Template:

var templates = Vector<Template>()val rawSample = createRawSample(imageUrl)val template = recognizer.processing(rawSample)templates.add(template)

Для создания Template используется метод recognizer.processing(), в качестве параметра передаётся RawSample. После того, как список с шаблонами лиц сформирован, его необходимо добавить в Recognizer и сохранить полученный TemplatesIndex, который нужен для быстрого поиска в больших базах:

val templatesIndex = recognizer.createIndex(templates, SEARCH_THREAD_COUNT)

На этом этапе, нами был сформирован объект Recognizer, который содержит всю необходимую информацию, чтобы произвести идентификацию:

fun detectFaceSearchResult(rgbPath: String): Recognizer.SearchResult {    val rawSample = createRawSample(rgbPath + COLOR_IMAGE_FORMAT)    val template = recognizer.processing(rawSample)    val searchResult = recognizer.search(            template,            templateIndex,            searchResultCount,            Recognizer.SearchAccelerationType.SEARCH_ACCELERATION_1    ).firstElement()    return searchResult}

Функция recognizer.search() вернёт нам результат, где мы можем получить индекс найденного элемента, сопоставить его со списком лиц из базы данных и идентифицировать персону. Кроме этого, мы можем узнать величину сходства, действительное число от 0 до 1. Данная информация предоставлена в классе Recognizer.MatchResult, переменная scope:

val detectResult = detectFaceSearchResult(rgbPath)// Величина сходства шаблонов - действительное число от 0 до 1.val scoreResult = detectResult.matchResult.score

Заключение


Нет сомнения, что в будущем подобные системы будут использоваться повсеместно: подходя к подъезду, дверь будет автоматически открываться, а кофемашина в офисе будет выбирать вашу любимую степень помола.

В Android SDK, постепенно добавляется API, который позволяет разработчику работать с системой идентификации лиц, однако сейчас всё находится на начальном этапе развития. А если говорить о системе контроля доступа с использованием планшета на Android, библиотеки Face SDK и 3D камеры Intel RealSense, хочется отметить большую гибкость и расширяемость. Нет привязки к устройству, камеру можно подключить к любому современному смартфону. Можно расширить линейку поддерживаемых 3D камер, а также подключить несколько штук к одному устройству. Есть возможность адаптировать написанное приложение под Android Things, и использовать его в своем умном доме. Если посмотреть на возможности библиотеки Face SDK, то с её помощью мы можем добавить идентификацию лиц в непрерывном видеопотоке, определять пол, возраст и эмоции. Эти возможности дают простор для множества экспериментов. А мы на своём опыте можем сказать: не бойтесь экспериментов и бросайте вызов себе!
Подробнее..

Не царская у тебя физиономия! Функции потерь для задачи распознавания лиц

08.12.2020 10:15:01 | Автор: admin

Кадр из фильма "Иван Васильевич меняет профессию"


Помните этот момент из легендарного произведения Гайдая? Удивительно, насколько по-разному может восприниматься один и тот же человек с одним и тем же лицом. А когда речь идет о миллионах разных людей и нужно найти одного единственного даже человек уже бессилен, а сверточные нейросети продолжают справляться. Такое большое количество лиц вынуждает искать новые подходы к разграничению. Один из таких подходов модификации функций потерь, которые помогают нам не потонуть в огромных датасетах при распознавании лиц, довольно точно определяя, кто есть кто.


Под катом мы рассмотрим различные модификации кросс-энтропии для задачи распознавания лиц.


Немного терминологии в контексте нашей задачи:


  • Бэкбон (backbone) некий черный ящик, сверточная сеть, входом которой является изображение лица, а выходом вектор, представляющий лицо. Примером может быть бэкбон из известного в узких кругах InsightFace Open Source решения для распознавания лиц с достаточно высоким качеством работы.
  • Вектор лица (embedding) вектор, представляющий лицо в многомерном пространстве. Размерность пространства обычно находится в пределах от 128 до 512. В нашей статье зафиксируем размерность вектора константой, равной 512. К векторам лиц мы предъявляем следующее требование: мы хотим их удобно сравнивать на основе некого расстояния между векторами. Другими словами, мы хотим, чтобы расстояние между векторами лиц одного человека было маленьким, а между векторами разных людей большим. Для этого требуем, чтобы скалярное произведение двух нормализованных векторов лиц (расстояние между векторами) одного и того же человека было бы как можно ближе к 1, а разных к -1 (ну или хотя бы 0).
  • Embedding-слой полносвязный слой, является последним и выходным слоем бэкбона, его размерность (количество нейронов) равна размерности вектора лица.
  • Слой классификации полносвязный слой, который иногда (зависит от функции потерь) следует за бэкбоном, его цель из вектора лица получить вектор вероятностей для классов, где каждый класс один человек. В общем случае его результат это $W^TX + b$, где $X$ вектор лица (выход бэкбона), $W$ веса полносвязного слоя, а $b$ свободный член. Переход от значений слоя к вероятностям обычно делается оператором softmax.

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


Существующие подходы к задаче распознавания лиц


Исторически в глубоких сверточных нейросетях существует два основных подхода к задаче распознавания лиц: обучение метрики (metric learning) и классификация.


Обучение метрики


В случае обучения метрики, мы заставляем сеть увеличивать расстояние между фотографиями разных людей и уменьшать его между фотографиями одного и того же человека. Самый известный пример Triplet Loss:


$Loss = \sum_{i=0}^N[||f_{i}^a - f_{i}^p||_{2}^2 - [||f_{i}^a - f_{i}^n||_{2}^2 + \alpha],$


где:


  • $f^a$ anchor, вектор лица человека, с ним сравниваются два других вектора;
  • $f^p$ positive, вектор другого изображения того же человека;
  • $f^n$ negative, вектор лица другого человека;
  • $\alpha$ некая константа, которая отвечает за минимальное расстояние между позитивной и негативной парами.

Таким образом, мы хотим, чтобы расстояние между фотографиями одного человека было как минимум на $\alpha$ меньше расстояния между фотографией того же человека и какого-то другого.


Классификация


Добавляем к нашему бэкбону полносвязный слой для классификации, обучаем сеть классифицировать людей (каждый класс отдельный человек), затем удаляем этот слой, и оказывается, что embedding-слой удовлетворяет нашим требованиям.


Идея Triplet Loss красивая и напрямую соответствует нашей задаче, а классификация отдает немного кашей из топора, но так вышло, что качество второго подхода лучше, все SotA обучены этим подходом, и основное развитие идей пошло по этому пути. Однако, Triplet Loss до сих пор можно использовать для дообучения (fine-tuning) модели. Так как нам интересен подход с лучшим качеством, рассмотрим подробней второй вариант и проследим историю развития идей.


Классификация в контексте распознавания лиц


Рассмотрим подробнее, что происходит, когда мы обучаем модель классифицировать людей, и почему потом можно использовать вектора лиц из, по сути, промежуточного слоя. На верхнем уровне путь от изображения лица до предсказания человека выглядит так:
Классификация
Веса слоя классификации это двумерная матрица, где первая размерность отвечает за вектор лица (в нашем случае 512), а вторая за количество классов (зависит от датасета, обычно десятки или сотни тысяч). Другими словами для каждого класса (человека) есть некий вектор, размерность которого равна размерности вектора лица. Этот вектор обычно называется центроид, то есть некий средний вектор конкретного человека. Весь процесс обучения теперь выглядит так: дай мне такой вектор фотографии, чтобы ближайшим из всех центроидов оказался центроид, соответствующий человеку на фотографии. Центроиды не фиксированы и изменяются в процессе обучения. Если рассматривать веса слоя классификации, то это будет выглядеть примерно так:



У классификации есть огромный плюс: каждое изображение мы сравниваем за один раз с десятками тысяч других изображений, а минус заключается в том, что это не то чтобы изображения, а некоторая обучаемая нами усредненная сущность человека. Несмотря на это, полученная модель хорошо учится отличать именно лица. Более того, время обучения остается контролируемым, в отличие от Triplet Loss когда имеем дело с уже неплохой моделью, становится крайне сложно искать тройки изображений, на которых сеть ошибается (hard sampling), поэтому обучение сети стремительно замедляется.


Просто посчитать дистанции от входного изображения до каждого из центроидов недостаточно непонятно, как нам за это штрафовать, поэтому для полноты картины нам не хватает двух типичных для классификации составляющих: Softmax для перехода от расстояний к вероятностям предсказаний и Cross Entropy для вычисления штрафа. В литературе они часто идут вместе и называются объединённо Softmax Loss.


Softmax Loss


Начнем с Softmax, который переводит полученные дистанции в вероятности предсказания. Введем обозначения:


  • $W$ веса слоя классификации (центроиды)
  • $X$ выход embedding-слоя, вектор входного изображения
  • $b$ bias, свободный член

Таким образом, выход слоя классификации (ни у него, ни у embedding-слоя нет активаций): $z =W^TX + b$. Функция Softmax($\sigma$) выглядит следующим образом:


$\sigma(z)_j = \frac{e^{z_j}} {\sum_{i=0}^C e^{z_i} }$


Следующий шаг добавить Cross Entropy, итоговый Softmax Loss имеет следующий вид:


$L_{Softmax} = - \frac {1}{N} \sum_{i=1}^N log\frac {e^{W^T_{y_i}x_i + b_{y_i}}}{\sum_{j=1}^C e^{W_j^Tx_i + b_j}}, $


Где $y_i$ индекс центроида нашего класса. Например, если на входе у нас фото Гарольда, и индекс класса Гарольда 42, то мы берем 42-ю строку в весах слоя классификации.


Все это классика и известно давно, а дальше самое интересное.


Подгоняем действительное под желаемое


Еще раз посмотрим на выход слоя классификации: $W^TX + b$ Чтобы свободный член не мешался уберем его: $b = 0$, получаем $W^TX$. Данное произведение является не чем иным, как скалярным произведением вектора лица на каждый из центроидов. Разберемся с размерностями:


  • $X$ имеет размерность $F\times B$, где $B $ размер батча (сколько изображений подаем на вход сети за одну итерацию), $F $ размерность вектора лица (у нас 512).
  • $W$ имеет размерность $F\times C$, где $C$ количество классов.

После умножения $W^T$ на $X$ получаем матрицу размером $C\times B$, то есть результат умножения каждого входного изображения на каждый из центроидов.


Теперь вспомним, как выглядит формула косинуса угла между двух векторов:


$cos(\theta) = \frac {dot(u,v)}{||u||||v||}$


Это как раз то, что нам нужно: угол между конкретным центроидом и вектором лица это скалярное произведение, деленное на две нормы. Нам мешают нормы векторов, чтобы это исправить, сделаем норму каждого центроида равной единице ($||W_i|| = 1$), а норму $X$ приравняем к некоторой константе s (scale), таким образом:


$W^TX = s*cos(\theta)$


Scale наш первый из двух гиперпараметров. Фиксирование его для всех векторов приводит к тому, что они теперь располагаются на гиперсфере. В 2D варианте это выглядит примерно так (один цвет один класс):

Изображение из статьи ArcFace, игрушечный пример для демонстрации: каждый класс выделен своим цветом, каждая точка на окружности отдельно взятое изображение, средний вектор каждого класса соединен с центром для наглядности. Обратим внимание, что классы соединены "без зазоров".


Перепишем Softmax Loss (теперь это называется Normalized Softmax Loss, N-Softmax) с учетом этих наблюдений:

$L_{N Softmax} = - \frac {1}{N} \sum_{i=1}^N log\frac {e^{s*cos(\theta_{y_i})}}{e^{s*cos(\theta_{y_i})} + \sum_{j=1, j\neq y_i}^C e^{s*cos(\theta_j)}}$


Мы разделили сумму в знаменателе на два слагаемых для удобства дальнейшего объяснения. На основе N-Softmax основаны все основные функции потерь для распознавания лиц.


Margin-Based Loss


Итак, у нас есть некие центроиды и текущий вектор лица, а после слоя классификации косинусы углов между ними. Если мы будем обучать с обычным softmax loss, то сети будет достаточно просто разделить классы между собой. Другими словами, у каждого человека есть свое пространство (decision boundary), в которое должны попадать все вектора его лиц (в идеальном случае). Границы этих пространств могут быть близки друг к другу. Почему это проблема? Потому что расстояние (угол) между изображениями на границах близких классов может быть меньше, чем расстояние между некоторыми изображениями одного класса. Тут возникает идея давайте добавим между этими пространствами некоторую пустую область (decision margin). Размер этой области margin второй главный гиперпараметр наших функций потерь, о первом мы говорили ранее: scale норма вектора лица. Графически на нашей 2D гиперсфере это выглядит так (рисунок из статьи ArcFace):



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


Так как мы говорим об углах между векторами, margin (обозначим для краткости $m$) можно добавить в три места в функцию потерь:


  1. Домножить на угол. То есть для позитивного случая вместо $cos(\theta)$ будет $cos(m*\theta)$. Этот метод используют две работы: Large-Margin Softmax Loss и SphereFace. Этот подход не хватает звезд с неба, и интересен скорее с исторической точки зрения, так как был первым вариантом Margin-based loss. Функция потерь выглядит следующим образом:


    $L_{SphereFace} = - \frac {1}{N} \sum_{i=1}^N log\frac {e^{s*cos(m*\theta_{y_i})}}{e^{s*cos(m*\theta_{y_i})} + \sum_{j=1, j\neq y_i}^C e^{s*cos(\theta_j)}}$


  2. Отнять margin от косинуса угла. Теперь вместо $cos(\theta)$ у нас $cos(\theta) - m$ для позитивного случая. Про это тоже две основных работы: AM-Softmax и CosFace , что порождает некоторую путаницу, так как встречаются оба названия, но обе статьи про одно и то же. Функция потерь:


    $L_{CosFace} = - \frac {1}{N} \sum_{i=1}^N log\frac {e^{s*cos(\theta_{y_i}) - m}}{e^{s*cos(\theta_{y_i}) - m} + \sum_{j=1, j\neq y_i}^C e^{s*cos(\theta_j)}}$


  3. Прибавить margin непосредственно к углу: $cos(\theta)$ $cos(\theta + m)$. Эта идея показана в ArcFace. Функция потерь ArcFace имеет следующий вид:


    $L_{CosFace} = - \frac {1}{N} \sum_{i=1}^N log\frac {e^{s*cos(\theta_{y_i}+m) }}{e^{s*cos(\theta_{y_i}+m) } + \sum_{j=1, j\neq y_i}^C e^{s*cos(\theta_j)}}$


  4. Хочется отметить еще один вариант, он является развитием идеи ArcFace и называется AirFace. Margin так же, как и у ArcFace, добавляем к углу, но уходим от косинуса угла непосредственно к самому углу ($arccos(cos(\theta)) = \theta$). Чем дальше векторы друг от друга, тем больше угол, а нас это не особо устраивает (почему будет ниже), поэтому авторы добавляют немного эвристики, и теперь у нас не просто $\theta$, а $(\pi - 2\theta)/\pi$, и итоговая функция потерь имеет следующий вид:


    $L_{AirFace} = - \frac {1}{N} \sum_{i=1}^N log\frac {e^{s*(\pi - 2(\theta_{y_i}+m))/\pi}}{e^{s*(\pi - 2(\theta_{y_i}+m))/\pi} + \sum_{j=1, j\neq y_i}^C e^{s*(\pi - 2\theta_j)/\pi}}$



Три разновидности добавления margin добавление к углу, к косинусу угла и домножение на угол легли в основу многих модификаций (особенно ArcFace).


Margin & Scale


Мы более-менее разобрались, в чем идея развития функций потерь для распознавания лиц, но у нас есть два гиперпараметра scale (s) и margin (m), влияние которых пока не очевидно. Например, в статье AM Softmax предлагается брать $s = 30$, а $m = 0.35$, в ArcFace $s = 64$, a $m = 0.5$, а в CosFace (напомним, идея та же, что и AM Softmax) $s = 64$, a $m = 0.35$. В каждой статье приведены теоретические обоснования, почему параметры такие, но в целом это скорее подобрали эмпирически, о чем авторы честно пишут.


Тут на сцену выходит еще одна работа AdaCos, основная идея которой исследовать влияние scale и margin на предсказанную вероятность позитивного случая. Основные постулаты следующие:


  • Margin и scale зависят от количества классов вполне логичное умозаключение, о чем в предыдущих работах не особо упоминалось.
  • Авторы делают вывод о зависимости параметров scale и margin, фиксируют margin и смотрят влияние scale.
  • Предлагается формула для вычисления scale в зависимости от количества классов.
  • Авторы предлагают два варианта обучения с фиксированным и изменяемым scale
    В статье есть красивые графики для 2 тысяч и для 20 тысяч классов, приведем их без изменений, по оси Y у нас предсказание вероятность, что фото относится к своему классу, а по X угол между центроидом и вектором лица. Чем меньше вероятность, тем больше штраф, если вероятность равна единице, то штрафа нет:

    В верхнем ряду показано влияние scale (у авторов явно не указано, но можно предположить, что margin фиксирован и равен 0), в то время как в нижнем margin при scale=30. Как видно, чем больше scale, тем резче ступенька, а margin двигает ее вдоль оси X. Очевидно, что одинаково плохи граничные случаи слишком большие и слишком малые scale и margin, и истина где-то посередине, но где? В AdaCos предлагается выбор scale на основе количества классов (на графиках выше изображено пунктиром). Пропуская вывод, к которому есть вопросы: $s = \sqrt{2}*\ln{(C-1)}$, где $C$ количество классов. На основе этой формулы для большинства задач распознавания лиц s попадает примерно в диапазон [10, 25], что значительно меньше предлагаемых ранее.

    Последнее нововведение работы AdaCos динамичное изменение scale. В основном, scale уменьшается, формула пересчета основана на среднем угле в батче. В результатах, приведенных в статье, это улучшает качество.

    Сравнение функций потерь


    Мы можем двигать margin куда и как угодно, но как это работает, и почему у нас есть разница между вариантами, и есть ли вообще? Разберем академический подход и построим зависимость целевого значения от угла для позитивного случая. По оси X берем угол между центроидом и вектором лица, а по оси Y полученное значение функции с учетом наших манипуляций (для обычного N-softmax это будет $cos(\theta)$):

    Из интересного: CosFace ожидаемо просто сдвигает N-softmax вниз, а ArcFace влево. SphereFace, согласно оригинальной идее имеет смысл в пределах $\frac {\pi}{margin}$, в нашем случае до $\frac{\pi}{3}$. У ArcFace есть неприятный хвостик с увеличением угла увеличивается target logit, что не очень хорошо, так как на этом участке чем больше угол, тем лучше. Другими словами, мы отдаляем фотографии человека от его центроида, если они в зоне этого хвоста (угол больше $\frac{5\pi}{6}$). Про этот момент в оригинальной статье не особо сказано, но при этом в реализациях (например, тут) есть небольшой интересный кусок кода, который его исправляет:

    # cosine - cos(theta)# phi - cos(theta + m)# th - cos(math.pi - m)# mm - sin(math.pi - m) * mif easy_margin:  phi = torch.where(cosine > 0, phi, cosine)  else:  phi = torch.where(cosine > th, phi, cosine - mm)
    


    Нечто под названием easy_margin и его антагонист направлены именно на устранение хвоста, графически это выглядит так:



    Easy margin заменяет поведение во всей зоне, где угол между вектором текущего лица и центроида больше $\frac{\pi}{2}$ на обычный N-Softmax ($cos(\theta)$), а not easy margin меняет только проблемный кусок. Однако, как показывает практика, даже случайная инициализация приводит к тому, что медиана углов находится в $\frac{\pi}{2}$, и даже негативные кейсы очень редко попадают в зону хвоста, а позитивные так и вовсе в качестве исключения, так что это дополнение мало сказывается на результате, но учесть это будет не лишним.


    Численные оценки качества приведены в каждой из работ, но для чистоты возьмем только независимые обзоры. Если коротко "в среднем" побеждает ArcFace. Например, в обзорной статье показано (таблицы 4 и 8 в работе):


    Loss LFW MegaFace, Rank1 @ $10^6$ MegaFace, Tar @ Far $10^{-6}$
    AM-Softmax/CosFace 99.33 0.9833 0.9841
    ArcFace 99.83 0.9836 0.9848
    SphereFace 99.42 0.9743 0.9766

    В другом обзоре тенденция та же (рисунок 3 в работе), тесты на LFW, заголовок столбца бэкбон-тренировочный датасет:


    Loss Resnet50-MSC MobileNet-MSC Resnet50-Casia MobileNet-Casia
    AM-Softmax/CosFace 99.3 97.65 99.34 98.46
    ArcFace 99.15 98.43 99.35 99.01
    SphereFace 99.02 96.86 99.1 97.83

    Авторы приходят к выводу, что ArcFace является SotA в вопросе функций потерь.


    Заключение


    Мы рассмотрели основные варианты функций потерь для задачи распознавания лиц. Все они заключается в добавлении дополнительного пространства между классами (margin) к предсказаниям сети, что позволяет значительно повысить качество. Из существующих вариантов стоит отметить AM Softmax (за счет простоты и очевидности) и ArcFace (лучшие результаты по большинству тестов). Для небольших сетей, где важна скорость, и не так важны десятые процента точности, можно рассмотреть AirFace.


    Литература


    Функции потерь:


    SphereFace https://arxiv.org/abs/1704.08063
    AM Softmax https://arxiv.org/abs/1801.05599
    CosFace https://arxiv.org/abs/1801.09414
    ArcFace https://arxiv.org/abs/1801.07698
    AirFace https://arxiv.org/abs/1907.12256


    Обзоры:


    Deep Face Recognition: A Survey https://arxiv.org/abs/1804.06655
    A Performance Evaluation of Loss Functions for Deep Face Recognition https://arxiv.org/abs/1901.05903

Подробнее..

Обзор технологий трекинга AR Маски

02.02.2021 02:11:38 | Автор: admin

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

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

Применение AR масок

AR маски наверное самый распространённый вид применения дополненной реальности. Иногда даже складывается впечатление, что многие не воспринимают его, как AR. Первое же что приходит в голову это социальные сети. Маски с дополненной реальностью уже неотъемлемая часть того же Instagram и TikTok. Но это далеко не единственное использование технологии. Приведу ещё немного примеров.

Face Tracking в продакшене

Покупка Epic Games технологии Hyprsense.

В 2020 году у нас состоялась интересная сделка. В статье, конечно, говорится о том, что анимации в фортнайт. Это возможно, но есть и другая сторона вопроса. В игровой разработке и в видео продакшене многие пытаются перейти от дорогостоящего производства лицевых анимаций с помощью Motion Capture технологий, таких как Faceware, к более простым инструментам. FaceRig используется VTuberами. Да и во многих случаях не нужна точность, которую предоставляют дорогостоящие технологии захвата движений. Даже в проектах, которые мне когда-то нужно было разработать встречался такой подход, что часть лицевых анимаций записывались с помощью трекинга ARKit. Поэтому одно из применений это упрощённый демократичный путь производств, который подходит тем же инди студиям, у которых нет бюджета на закупку оборудования за несколько тысяч долларов. Хотя сравнительно недавно у Faceware появилось предложение для инди.

AR Маски в рекламе

Реализация довольно похожа на то, что мы видим в социальных сетях. Макси используются для продвижения какого-либо продукта или привлечения аудитории. Так же для создания аудиторией собственного контента. Ещё несколько примеров:

https://player.vimeo.com/video/220504292

https://mixr.ru/2021/01/20/trollo/

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

Технологии

Большая часть разбираемых технологий будет совместима с Unity, но кроме того я встречал интересные решения для веба, да и некоторые нельзя просто обойти стороной. Отфильтрованы были те, у которых нельзя скачать SDK для того, чтобы самостоятельно проверить работу SDK или же которые не совсем понятно, каким образом можно купить (если технология платная).

SparkAR

Цена: Бесплатно

Поддерживаемые платформы: Android/IOS

Совместимость с Unity: Нет, но можно сделать ссылку в приложении

Поддержка устройств: Большое количество устройств

https://sparkar.facebook.com/ar-studio/

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

Основным минусом является то, что данное решение нельзя встроить в ту же стойку, оно в любом случае уводит вашего пользователя из приложения, и если у пользователя нет инстаграма - он не сможет ей воспользоваться. Так же в недостатки я бы отнёс то, политики фейсбука, некоторую ограниченность инструментов рендера внутри Spark AR Studio и некоторую закрытость технологии.

ARFoundation (ARKit/ARCore)

Цена: Бесплатно

Поддерживаемые платформы: Android/IOS

Совместимость с Unity: Есть

Вес SDK в билде: Около 2МБ

Поддержка устройств: Маленькое количетсво устройств

https://docs.unity3d.com/Packages/com.unity.xr.arfoundation@4.1/manual/

В версии ARFoundation 4.1 Face tracking поддерживается уже и ARCore и ARKit (Достаточно долгое время ARFoundation поддерживал только ARCore). Отдельно на самих технологиях останавливаться не хочется, так как они имеют не так много различий и, по сути, объединены общим API в AR Foundation. Из плюсов встраиваемость в Unity приложение, стабильная работа, относительная простота интеграции и бесплатность. Основным же минусом является небольшое количество поддерживаемых устройств.

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

OpenCV + Dlib

Цена: Бесплатное/реализация для Unity 135$ (https://assetstore.unity.com/packages/tools/integration/opencv-for-unity-21088 + https://assetstore.unity.com/packages/tools/integration/dlib-facelandmark-detector-64314 )

Поддерживаемые платформы: Android/IOS/WebGL/Win/Mac/Linux/Hololens/MagicLeap

Совместимость с Unity: Есть

Вес SDK в билде: около 9МБ (без классификаторов, но их можно скачать по сети)

Поддержка устройств: Зависит от требуемого функционала

https://opencv.org/

Последняя условно бесплатная технология. Условно бесплатная, потому что в целом можно взять бесплатные библиотеки, написанные на том же Python, развернуть сервер обработки и слать данные куда душе угодно. В случае локальной сети это даже возможно сделать в реальном времени. Что же касается реализации для Unity - если не писать порт самостоятельно, то она платная. Основной нюанс OpenCV и Dlib, что это не готовые технологии и с ними надо уметь работать. К плюсам можно отнести широкую поддержку платформ. Работает оно почти везде и умеет очень многое. Даёт больше контроля, позволяет самостоятельно настраивать качество классификаторов и многое другое. На Python огромное количество материалов для изучения, которые легко переносить в Unity поняв суть SDK.

Из минусов - очень высокий порог входа. Для того, чтобы комфортно работать на OpenCV в реалтайме и не иметь проблем с производительностью - нужно очень много знать. Во-первых, знать OpenCV достаточно хорошо. Во-вторых, знать, как работать с многопоточностью в Unity, да и в целом знать Unity достаточно глубоко. Cтоит ввязываться, только если интересно разобраться в теме компьютерного зрения или разработки своей технологии. Или не лезь, оно тебя сожрёт. Базовые примеры на андроид выдают 2 FPS, но я реализовывал и стабильные 30, и стабильные 60 на средних устройствах закапываясь в это SDK с головой.

XZIMG

Цена:от 2100

Поддерживаемые платформы: Android/IOS/WebGL/Win

Вес SDK в билде: около 2МБ

Совместимость с Unity: Есть

Поддержка устройств: Большое количество устройств

https://www.xzimg.com/Products?nav=product-XMF

Основной плюс данного SDK в коммерческих проектах при широкой поддержке устройств - что это не подписка, а лицензия за которую надо заплатить один раз. В ряде проектов при сметировании это в разы удобнее, чем учитывать подписку, которая ещё в свою очередь зависит от нагрузки. Хотя цена, конечно, немаленькая. По качеству трекинга работает хорошо, даже на слабых устройствах типа MOTOROLA G5S. Ну и Android 4.1 - это очень широкий спектр устройств. Судя что по сайту, что по проекту примеру - делали инженеры. Тут есть ряд доработок, которые сильно улучшат перфоманс этого решения. В целом без опыта работы в OpenCV неплохое коробочное решение.

ARGear

Цена: Бесплатно/от 25$ в месяц

Поддерживаемые платформы: Android/IOS

Совместимость с Unity: Есть

Вес SDK в билде: около 9МБ

Поддержка устройств: Большое количество устройств

https://argear.io/

По ощущениям работает хуже, чем XZIMG. Подтормаживает даже на Xiaomi Redmi Note 8T. Проект пример на андроид плохо организован и без пляски с бубном не заводится. Но в целом с этим можно работать. В целом, как сервис по подписке выглядит неплохо, и не просит сразу платить за коммерческую лицензию несколько тысяч долларов, что думаю для кого-то будет плюсом. Больше всего расстроил, по сути, sample project для Unity, так как у меня sdk. которые не заводятся из коробки - не вызывают доверия.

BRFv5

Цена: Неизвестно

Поддерживаемые платформы: Web/Android(Chrome)/IOS(Safari)

Совместимость с Unity: Нет

https://github.com/Tastenkunst/brfv5-browser

Единственное целиком и полностью веб SDK с непонятным ценообразованием, которое в первую очередь понравилось мне своим качеством. Пример его работы можно посмотреть тут. Но как же оно хорошо работает. Я протестировал его и на своих тест устройствах на смартфонах (важно: на андроид работает именно в хроме) и на пк погонял, и для 68 лендмарок это прям круто. В плюс к тому это веб. Прям даже интригующе. Качество трекинга в разы лучше, чем у того же Media Pipe графа для построения меша лица.

В заключении

Из не вошедших, пожалуй, стоит упомянуть Banuba и Deepar. Бануба не вошла, так как у неё непонятное ценообразование и как бы там ни было сложная схема получения триала. А Deepar так как у него отсутствует SDK для Unity, и при этом он мне не показался особо интересным.

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

Всем спасибо за внимание, поддержите если материал был интересен и стоит продолжать эту серию. Пока в планах стоит ещё маркерный трекинг, SLAM, системы оптического трекинга и т.п.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru