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

Распознавание лиц

Из песочницы Защита фото от систем распознавания лиц работает?

22.09.2020 16:11:47 | Автор: admin
image

За последние полтора месяца (с начала августа 2020) уже довольно много изданий/платформ и ресурсов говорили/писали про Алгоритм Fawkes: https://sandlab.cs.uchicago.edu/fawkes/#press.

Среди которых и Habr, The New York Times, The Verge и т.д.

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

Исследователи из Чикагского университета придумали алгоритм клоакинга, для защиты от распознавания лиц. Выложили исходники на github: https://github.com/Shawn-Shan/fawkes.

В августе я прочитал про этот инструмент (Алгоритм Fawkes). И решил заменить свои фото в социальных сетях и на всех интернет ресурсах, где есть мои реальные фото.

Но, для начала, решил испробовать данный инструмент на своих фото:

  • запускается ли fawkes;
  • проверить собственными глазами результат работы fawkes;
  • проверить на заявленных ресурсах (теми же инструментами, что и автор fawkes проверял свой алгоритм).

Благо на https://github.com/Shawn-Shan/fawkes есть довольно подробная и простая инструкция по работе с fawkes.

Создателем fawkes заявлено, что алгоритм защищает от:

  • Microsoft Azure Face API,
  • Amazon Rekognition Face Verification,
  • Face++ Face Search API.

Данный список указан в Technical Paper:

image

На личном сервере собрал из исходников: git clone; pip3 install fawkes. Это было не просто, а очень просто.

Закинул свое фото на сервер r1.jpg. И по инструкции обработал это фото с помощью fawkes.
На выходе получил второе фото: r1_min_cloaked.png. Ура, я получил клоакнутое фото. Открыл фото r1_min_cloaked.png посмотреть своими глазами. Изменения заметны, но не критичны. Вокруг глаз, переносицы и носа есть не значительные затемнения.

image

После этого решил проверить результат (r1_min_cloaked.png) на сервисах Microsoft Azure Face API, Amazon Rekognition Face Verification и Face++ Face Search API.

Результат:

r1-and-r1_cloacked

Как видим нейросеть Microsoft Azure Face API показала, что оригинальное фото (слева на скриншоте) и фото после обработки (справа на скриншоте) один человек. Аналогичные цифры показали и остальные инструмента проверки: нейросети Amazon Rekognition Face Verification и Face++ Face Search API.

То же самое с защитой фото других людей/персон:

r1_and_cat

obama_origin_and_cloacked

emily_origin_and_cloacked

queen_origin_and_cloacked_faceplusplus

obama_origin_and_cloacked_faceplusplus

То есть по состоянию на середину сентября Fawkes не работает. Возможно, конечно, Fawkes работала в августе 2020 года. Но в сентябре 2020 году уже не работает.

Неделю назад писал письмо разработчику Fawkes и его команде Fawkes team, с просьбой помочь подтвердить работу алгоритма. Но ответного письма пока не получил.

На данный момент я так и не смог подтвердить работу Fawkes.
Подробнее..

РосКомСвобода на ОГФ2020 рассказываем про открытые данные о пандемии и праве на приватность

17.12.2020 16:16:33 | Автор: admin
image

РосКомСвобода совместно с Инфокультурой весь день вела на Общероссийском гражданском форуме (ОГФ'2020) площадку Право на приватность и открытость.

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

Ключевые цитаты из выступлений:



Все видео выступлений








Ссылки на полные обзоры в конце статьи.

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

image

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

Кто же акторы нарушения наших цифровых прав, нашего права на приватность? Это государства, корпорации и киберпреступники.


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

image

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

Массово стали использовать распознавание лиц не только для борьбы с преступностью, но и для отслеживания нарушителей карантина.


Pandemic Big Brother интерактивная карта, на которой фиксируются случаи нарушения цифровых прав граждан, такие как введение слежки, цензуры и контроля за коммуникациями.

Юристы РосКомСвободы готовы помочь всем, кто пострадал из-за незаконной слежки. Ваши обращения вы можете присылать на адрес legal@roskomsvoboda.org

image

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

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


Единственный подходящий закон о защите во время чрезвычайных ситуаций. Он, к слову, не содержит ничего об ограничении прав, которые власти применяли с таким упорством, но только то, что должны делать МЧС и другие органы по координации своих действий во время ЧС. Федеральная власть не хотела рисков и потому поступила хитро, а именно: переложила все решения на субъекты. Как результат Москва начала принимать свои законы, которые вошли в диссонанс с федеральным законодательством. Даже если бы ковида не существовало, его стоило бы придумать, по крайней мере для московских властей, которые годами держали эти технологии под сукном и ждали возможность начать их использовать, сказал Дарбинян. По его словам, различные технологии для слежки у московской мэрии были давно, и пандемия послужила спусковым крючком для их использования.

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



Кандидат биологических наук, независимый аналитик Алексей Куприянов представил доклад об общественном аудите государственной статистики. С 13 марта эксперт собирает данные по распространению коронавирусной инфекции по России. В апреле он стал публиковать их и в Фейсбуке на страничке инициативной группы Watching Covid-19.ru, где вместе с коллегами, в том числе другими докладчиками Алексеем Ракша и Борисом Овчинниковым, выкладывает аналитику. Основные источники данных Стопкоронавирус.рф и СМИ, на первых порах ещё бюллетени Роспотребнадзора.

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

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

Эта пандемия развивается благоприятно в плане открытости данных, но она совершенно проиграна в плане достоверности данных.


Для получения более точных данных, по мнению учёного, необходимы:
машиночитаемые данные;
доступ к дезагрегированным данным;
расширение спектра доступных параметров;
смещение акцента с агрегирования на валидацию.

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

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

image

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

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

Общая численность населения неточна, например, из-за разницы между переписью населения и учётом за обозначаемый период. Также есть проблема подсчёта числа людей в регионах с интенсивной трудовой миграцией. Заболеваемость трудно исчислять из-за проблем диагностики, неучтённости повторных диагнозов и пр. Что касается смертности, то 74% врачей подтверждают в опросах те или иные манипуляции с кодировкой смертности, в двух случаев из трёх это происходит под административным давлением.

Чем больше интерес к какому-то статическому показателю, тем больше этот показатель фальсифицируется.


По потребности в объёмах медпомощи последние исследования проводились ещё в СССР. Заявленные объёмы медорганизаций не обеспечены финансами, они непрозрачны и определены не научными методами. По обеспеченности кадрами нет единых, полных и научно обоснованных нормативов нагрузки. Многие нормативы сформированы 30-40 лет назад и отстают от возможностей современных технологий. Штатные расписания не соответствуют нормативам по медперсоналу. Нагрузка по ставкам искажена требованиями исполнения майских указов президента и не соответствует фактической.

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

Для решения проблем спикер предлагает:
сформировать единую научно обоснованную систему управленческого мониторинга;
открыть данные для профессионального сообщества;
упростить и устранить дублирование отчётности;
изменить управленческий вектор с наказания на мотивацию;
упростить порядок оплаты медпомощи.

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

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

image

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


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



Директор АНО Информационная культура Иван Бегтин говорил об общественном контроле за алгоритмами в аспекте безопасности раскрытия кода. По его словам, большая часть госсистем на данный момент не использует умные алгоритмы, но переход к принятию решений на основе ИИ постепенно происходит и у них. Примеры тому Банк России, Росфин, Правительство. Там, где есть трансфер денег, это используют, резюмировал спикер. Хотя происходит это крайне непублично.

Пока государство приближается к использованию ИИ, компании его уже внедряют ИИ. Однако у государства есть проблема работа с уклоном в наказания.
Если дошёл до суда, тебя либо оштрафуют, либо посадят, но оправдан ты не можешь быть. Главная функция адвоката развалить дело до суда. Шансов, что тебя оправдают, мало. Что хорошего в алгоритме, у которого выборка будет изначально тебя посадить, размышляет Бегтин.


Однако нерелевантную выборку можно поправить. Для этого, к примеру, в ЕС разрабатывается стандарт проверки алгоритмов, по которому ответственность за ИИ несут его разработчики. Если система что-то неправильно распознала оштрафуют её авторов.
Раскрыть данные можно и нужно, убеждён Бегтин. Но в России пока некому проверять их и следить за искусственным интеллектом. Для этого нужно повышать квалификацию чиновников.

image

Глава юридической практики Роскомсвободы, управляющий партнер Digital Rights Center Саркис Дарбинян тоже посетовал, что юриспруденция не развивается экспертно, она давно уже не наука.

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


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

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

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



Журналист Андрей Каганских продолжил тему приватности и рассказал об утечках данных московской системы распознавания лиц и как благодаря РосКомСвободе в полиции раскрыли занимавшихся пробивом по лицу сотрудников. Он интересовался темой давно, предположив, что, поскольку данные из всех госсистем утекают, скорее всего, сольют их и с камер распознавания лиц. В ноябре 2019 года он обнаружил, что, хоть система и не была запущена в полном объёме, с неё уже появились утечки:

Работали всего 2% камер, но с них уже вовсю банчили данные на чёрный рынок.


Об этом стали писать СМИ, которым Департамент информационных технологий Москвы отвечал одно и то же, хотя это явно противоречило объективной реальности: Доступ к данным ЕЦХД [Единый центр хранения и обработки данных Москвы прим. ред.] имеют только уполномоченные сотрудники органов исполнительной власти и правоохранительных органов.

К началу пандемии система видеонаблюдения заработала полноценно. Весной данные на чёрном рынке стали доступны за 1 тыс. долл. За полгода чиновники так и не остановили утечки данных с московских камер. РосКомСвобода мониторила ситуацию всю весну, а летом провела эксперимент: волонтёр Анна купила полное досье на себя за 15 тыс. руб. Кроме того, выяснилось, что барыги на чёрном рынке на просьбу показать, как работает слив данных, предоставили данные шести человек просто в качестве примера. Если бы я был полицейским, это было бы шесть эпизодов в уголовном деле, сказал Каганских.

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

В ноябре мэрия выделила 237 млн на реформу этой системы. Но можно израсходовать раз в 10 меньше денег на систему безопасности МВД, которая просто будет запугивать коррумпированных полицейских, которые сливают данные на чёрном рынке, считает Каганских. По его мнению, власти хотят исправить техническую часть системы, в то время как надо работать над юридической. Полиция, ДИТ не контактируют с людьми, поэтому расследование удалось провести благодаря тому, что полиция любит сливать данные на чёрный рынок и зарабатывать. Зарабатывать, кстати, гроши. Мы только догадываемся, как работает система распознавания лиц. К слову, это ярко иллюстрируют кейсы Сергея Межуева и Антона Леушина первые известные нам случаи ошибки этой системы.

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

В России есть огромная системная проблема со сливами: сотрудники силовых ведомств постоянно что-то сливают. Это значит, что службы безопасности МВД и ФСБ плохо справляются о своей работой.


image

Разработчик плагина CensorTracker РосКомСвободы Вадим Мисбах-Соловьёв представил инструмент для противодействия слежке в интернете.

CensorTracker умеет следующее:
определять наличие домена в реестрах ОРИ и заблокированных сайтов;
проверять блокировки по закону о суверенном Рунете;
предоставлять доступ к заблокированным доменам.

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

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

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

Мы гарантируем приватность пользователей, заявил Мисбах-Соловьёв.


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

Читайте подробные обзоры с секций по ссылкам ниже:
Открытость и приватность: дисбаланс между правами граждан и действиями государства в эпоху пандемии коронавируса здесь.
Открытые данные как инструмент общественного контроля борьбы с пандемией здесь.
Ответственные алгоритмы: как открытость способна повлиять на легитимность технологий в обществе? здесь.
Гражданские инициативы по приватности и открытости в период пандемии коронавируса здесь.
Подробнее..

Атака на Капитолий не оправдывает расширение слежки за населением

17.01.2021 18:05:05 | Автор: admin


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

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

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

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

Но главное вот в чём. Наделение полиции ещё большими полномочиями в этом кризисе станет гигантской ошибкой.

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



Использование государством распознавания лиц пересекает важную красную черту, и мы не должны разрешать рутинное его использование даже во время национальной трагедии, пишет EFF. Конечно, нападение на Капитолий может и должно быть расследовано. Но злоумышленники использовали общедоступные социальные сети как для подготовки, так и для документирования своих действий. Это значительно упрощает расследование. Запрет на любое государственное использование распознавания лиц, включая его использование правоохранительными органами, остаётся необходимой мерой предосторожности, чтобы защитить нас от этой опасной технологии.

Одним из сторонних сервисов по распознаванию лиц является ClearView AI. Он позволяет загружать фотографию неустановленного лица и получать публично опубликованные фотографии этого человека. Сообщается, что в последние дни нагрузка на сервис сильно возросла. Образцы фотографий в базе ClearView собраны без согласия миллионов пользователей по всему интернету, с Facebook, YouTube, Venmo и так далее. Там есть отпечатки лиц примерно 3 млрд человек.


ClearView AI

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

Это не вопрос Что произойдёт, если распознавание лиц будет использовано против вас? Вопрос в том, сколько раз правоохранительные органы уже делали это.

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



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

Широкое использование государством технологии распознавания лиц в реальном времени коренным образом изменит общество, в котором мы живём. Фонд электронных рубежей.

Многочисленные исследования показали, что когда люди думают, что правительство следит за ними, они меняют своё поведение, чтобы избежать пристального внимания.

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

Чтобы найти участников этого нападения, не нужны сверхсовременные системы слежки: они сами выследили себя. Они транслировали свои преступления в прямом эфире из залов Конгресса, записывая каждое преступление в full HD. Чтобы найти виновных, не нужны распознавание лиц и геозоны по сотовым вышкам, нужна просто нормальная полицейская работа.


Подробнее..

Анонимность в современном мегаполисе

21.04.2021 14:19:32 | Автор: admin


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


Эта статья о том, какие данные собираются, какими способами и как от этого защититься.


Зачем вообще нужно беспокоиться о своей приватности и анонимности? Я довольно часто слышу высказывания вроде Мне не от кого скрываться или Мне нечего бояться.
Есть три основных причины:


  1. Информационная.
    Кто владеет информацией тот владеет миром. У хранителя информации (например, у государства или частной компании-оператора данных) появляется дополнительный рычаг воздействия на человека, которым можно в нужный момент воспользоваться. Крайне актуально, если вы политический или общественный активист, или же ваш бизнес представляет для кого-то интерес.
  2. Человеческий фактор.
    Как бы оператор данных не уверял вас, что все данные в полной безопасности, с этими данными так или иначе работают люди, и иногда эти люди не брезгуют незаконной подработкой так называемым пробивом. Так возникает черный рынок данных, на котором сейчас можно купить информацию почти на любого человека. Сделать это может кто угодно, будь то очередной мошенник или сосед, затаивший на вас обиду.
  3. Ошибки идентификации.
    Если система распознавания лиц ошибочно примет вас за человека в розыске, у вас скорее всего возникнут неприятности как минимум, вас задержат. Несмотря на крайне малый процент ложных распознаваний, прецеденты периодически случаются (раз, два).

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


Поехали!


Глава 1. Системы распознавания лиц


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


Поскольку FindFace использовал базу лиц, выгруженную несколько лет назад из Вконтакте, то логично предположить, что в московской системе распознавания лиц теперь используется и эта база. Так что если вы когда-либо загружали себе в профиль свое фото, то оно могло попасть в их датасет.
Кроме того, есть и иные базы данных, по которым можно сопоставить ваше лицо с вашими персональными данными. Одной из таких баз является система Российский паспорт. В ней содержатся отсканированные фотографии, которые вы подавали с анкетой на получение паспорта гражданина РФ. Что любопытно, фотографии в этой базе бывают даже 90-х годов (если вы тогда получали паспорт).


Пример записи в этой системе


(с) BBC News


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


Достоверно известно, что московская система распознавания лиц работает на следующих камерах:


  • Установленных на турникетах (башенки с индикатором на уровне лица). Установка этих камер началась в 2019 году со станции Октябрьское поле, сейчас ими оборудованы все станции московского метро. Эти камеры работают в сочетании с планшетами у нарядов полиции на станциях туда приходит уведомление, если обнаружено лицо в розыске.

Выглядят они вот так:


Устанавливаются на турникетах на вход и на выход.


  • Подъездных камерах (напоминают видеодомофон, оборудованы ИК-подсветкой для работы в ночное время, крепятся так же обычно на уровне лица). Кроме подъездов, они встречаются на входах в некоторые общественных заведениях (школах, поликлиниках).
    Есть официальная карта их мест установки.
    Эти камеры бывают в трех вариантах от разных производителей: Beward , Hikvision и Dahua.

Они выглядят так:


Тут зачем-то поставили сразу две камеры от разных производителей.


  • Дверях некоторых вагонов метро. Летом 2020 года был объявлен тендер на установку камер с распознаванием лиц в четверги всех вагонов метро. Камеры устанавливаются возле каждого дверного проема, либо над дверями, либо сбоку проема.

Вот так вот


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


  • Наземный транспорт
    Летом 2020 года был объявлен тендер на закупку аппаратуры видеоаналитики для московских трамваев.


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



Распознаванием лиц занимаются не сами камеры они только транслируют видеопоток, распознавание происходит централизованно, на специальных серверах, занимающих 19 стоек в дата-центре ЕЦХД. Общая емкость хранилища составляла 28 Петабайт на 2019 год.
Там же находятся сервера, на которых размещены базы данных и фронт-энд системы.


Примерно так выглядит веб-интерфейс доступа к данным системы распознавания лиц


(с) BBC News


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


Пример выдачи запроса о перемещениях человека


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


Что делать?


Подписать петицию. Так же Роскомсвободой @roskomsvoboda был создан проект BanCam, целью которого является запрет использования технологии распознавания лиц. Советую заглянуть на сайт там много интересных материалов по теме.


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


  1. При входе в метро обязательно надевайте маску на лицо (к тому же, пандемия на дворе).
  2. Одной маски может быть недостаточно стоит дополнить ее кепкой.
  3. При проходе через турникет, оборудованный камерой, пригните голову. Чем больше угол между лицом и камерой тем хуже точность распознавания. А если нижняя часть лица закрыта маской, а глаза скрыты под козырьком кепки распознавать остается нечего.
  4. При входе в подъезд, оборудованный камерой с распознаванием лиц, придерживайтесь тех же правил.

Глава 2. Мобильная связь


Появление мобильных телефонов не только упростило общение, но и стало великолепным подарком для спецслужб. Стало возможным не только отслеживать перемещения контактного человека, но и получить список людей, находившихся когда-то в нужный момент в прошлом в пределах видимости конкретной сотовой вышки.
Точность определения местоположения зависит от плотности размещения сотовых вышек (или если более научно, то базовых станций, БС). За городом плотность невысокая, и точность может составлять до нескольких (десятков) километров, а в центре города наоборот, с большим количеством вышек она в районе нескольких сотен метров. Существуют так же более точные методы определения местоположения (т.н. Трилатерация определение расстояния по времени прохождения сигнала между тремя точками), но они требуют уже активного вмешательства в сеть оператора, и к счастью не имеют массового характера, а применяются таргетированно. Почитать про виды технологий можно тут , а здесь неплохая статья про то, как это работает у Мегафона.
У сотового оператора в специальной базе для каждого абонента сохраняется лог его перемещений, а именно CellID (идентификатор БС ) и время присутствия в зоне приема данной станции. Срок хранения таких данных (вероятно) может составлять от полугода и больше.
Сейчас в России могут принять закон, согласно которому получать такие данные можно будет даже без решения суда, а это значит, что пробивы подешевеют к ним будут иметь доступ все, кому не лень.


Что делать?


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


  1. Не регистрировать сим-карту для звонков на свое имя. Хорошим вариантом является разделение симок на белую ту, что зарегистрирована на вас, на нее можно привязать например банкинг или госуслуги, и серую ту, с которой вы будете звонить и указывать на сайтах онлайн-покупок. Бонус если на серую сим-карту позвонят из службы безопасности банка, вы сразу будете знать, что к чему.
  2. Сим-карту для звонков можно периодически менять чем чаще, тем меньше данных о вас сможет собрать оператор. При смене сим-карты нужно не забывать, что у оператора сохраняется IMEI телефона это уникальный номер модема, который передается при регистрации в сети. Если телефон расчитан на две сим-карты, то в нем будут два разных IMEI (которые иногда могут различаться всего на одну цифру нужно смотреть в настройках). Некоторые телефоны позволяют его менять.
    Если вы поставили серую симку в телефон (или слот), где стояла до этого белая с анонимностью можно попрощаться.
    Так же, тем, у кого имеются данные по местоположению всех абонентов сотовой вышки, не составит труда сопоставить две сим-карты, одновременно работающие в разных базовых станциях, поэтому белую сим-карту лучше держать выключенной, и включать по необходимости.
  3. Вы всех перехитрили и пользуетесь серой симкой. Тем не менее, идентифицировать вас при желании можно, сопоставив список ваших контактов и их данных (под контактами имеются в виду абоненты, которым вы чаще всего звоните).
    Решение не звонить и не отправлять смс через мобильную сеть, а использовать для этого мессенджеры. Не стоит так же забывать, что через сотового оператора звонки и смс идут в нешифрованном виде, и согласно Закону Яровой хранятся 6 месяцев.
  4. Ну и в некоторых случаях можно все же выключить телефон (заранее, чтобы последняя запомнившая вас вышка связи была далеко от того места, которое вы хотите посетить анонимно. Это особенно актуально для некоторых массовых мероприятий, на которых глушится связь (намеренно или за счет перегруженной сети) позвонить вы не сможете, а местоположение засветите.
  5. Наверное, стоит здесь упомянуть про GetContact сервис, позволяющий узнать, как вы записаны в чужих адресных книгах. Так можно узнать ваши реальные данные и многое другое, если кто-то из ваших контактов имел неосторожность поставить себе на телефон данное приложение, и их телефонная книга утекла на сервера GetContact. Подробнее можно почитать тут.

Глава 3. Платежные системы


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

Пример аналитики в приложении Сбербанка:


  • Сохранность денег.
    Ваша карта или электронный кошелек могут быть заблокированы в любой момент, если службе безопасности покажется, что с вашим кошельком происходит что-то неладное. И если в банке разблокировка займет пару дней и ваши деньги гарантированно станут доступны вам вновь, то в случае с платежными системами их можно лишиться навсегда.
    Если вы неосторожно репостите мемы в социальных сетях, то вы рискуете попасть в список Росфинмониторинга. Механизм такой:
    сперва возбуждается уголовное дело, затем вас вносят в реестр экстремистов итеррористов, причем попасть туда можно даже без решения суда достаточно постановления о признании человека подозреваемым по некоторым статьям УК.
    После попадания в этот список банки обязаны заблокировать все ваши счета (можно будет снимать только 10000р в месяц). Тут можно почитать пару историй, про то как люди с этим живут.

Что делать?


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


Глава 4. Транспорт


В наши дни очень сложно перемещаться анонимно, особенно если речь идет о переездах на дальние расстояния. И если с международными переездами (перелетами) все очевидно требуется прохождение пограничного контроля, то с междугородними перемещениями есть менее очевидные способы слежки.


  • Автомобили.
    Каждое транспортное средство имеет так называемый госномер, который уникален и по которому можно без труда установить человека или юрлицо, на которое он зарегистрирован.
    Номерные знаки стандартизированы и адаптированы под автоматическое распознавание комплексами видеофиксации (или по-другому, дорожными камерами). Эти камеры установлены на всех выездах из города. Некоторые из них используются не только для контроля скорости, но и для сбора статистики перемещений транспортных средств, причем не только находящихся в розыске, но и вообще всех подряд.
    Одной из таких систем контроля является система Паутина, почитать про которую можно тут и тут.
  • Самолеты, поезда и междугородние автобусы.
    Приобрести билет на эти виды транспорта сейчас невозможно без внесения данных паспорта. Эти данные используются не только для идентификации пассажира, но и попадают в специальную систему, которая называется Розыск-Магистраль, причем информация там появляется еще на стадии бронирования билета. Срок хранения информации о поездках в этой базе неизвестен.
  • Местный транспорт метро, электрички и рейсовые автобусы.
    Поскольку проезд за наличные (покупка билетов на одну поездку) сейчас в городе не выгодна действуют повышенные тарифы (кроме электричек), то все в основном пользуются транспортными картами.
    При проходе через турникет номер и время транспортной карты отправляются турникетом на сервер, поэтому логично предположить, что данные о перемещениях собираются. Сейчас так же появилась возможность сопоставить эти данные с фотографией, сделанной камерой на турникете.
    Если вы пользуетесь социальной картой москвича, то ваши паспортные данные (в том числе и фото) уже есть у перевозчика, поэтому говорить о какой-либо приватности поездок тут глупо.

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


Глава 5. Прочие способы слежки


  • Слежка через Wi-Fi или Bluetooth.
    В Москве работает Wi-Fi сеть на транспорте. Для того, чтобы воспользоваться этой сетью, необходимо привязать к устройству номер телефона. Оператор сети активно сотрудничает с московскими властями и рекламными компаниями.
    Собираются данные о действиях пользователей в сети, используемых сервисах, а так же геолокация.

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


Кроме городских властей, сбором данных о геолокации через Wi-Fi занимаются и частные компании, например 2wifi.


Что делать?


Не пользоваться городской Wi-Fi сетью, благо мобильный интернет сейчас работает на всех станциях подземки и даже в некоторых перегонах.


Пост-скриптум


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


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

Подробнее..

Сим-сим откройся как я научил дверь своего подъезда узнавать меня в лицо

07.06.2021 18:08:54 | Автор: admin

Пятничный рабочий день на удалёнке уже подходил к концу, как в дверь постучали, чтобы сообщить об установке нового домофона. Узнав, что новый домофон имеет мобильное приложение, позволяющее отвечать на звонки не находясь дома, я заинтересовался и сразу же загрузил его на свой телефон. Залогинившись, я обнаружил интересную особенность этого приложения даже без активного вызова в мою квартиру я мог смотреть в камеру домофона и открывать дверь в произвольный момент времени. "Да это же онлайн АРI к двери подъезда!" щёлкнуло в голове. Судьба предстоящих выходных была предрешена.

Видеодемонстрация в конце статьи.

Кадр из фильма Пятый ЭлементКадр из фильма Пятый Элемент

Дисклеймер

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

Получение API

Чтобы автоматизировать управление дверью, необходимо понять, куда и в каком формате отправляет запросы само приложение. Реверс-инжиниринг дело неоднозначное, поэтому я попытался обойтись без него и вместо этого просто перехватить свой собственный трафик. Для этой задачи я взял НТТР Тооlkit комплекс программ, который позволяет наладить прослушивание http(s) запросов собственного Android устройства.

Первая попытка оказывается провальной после установки на телефон Android-части инструментария и сгенерированного Certificate authority оказалось, что мобильное приложение домофона не доверяет пользовательским СА. Согласно документации, начиная с Android 7 манифест приложения должен явно изъявлять такое желание.

Так как мой телефон не поддерживает root доступ для модификации списка системных СА, я воспользовался официальным эмулятором Android, идущим в комплекте с Android Studio. После запуска эмулятора и перехвата с помощью ADB меня встретило радостное сообщение о том, что трафик от всех приложений без Certificate pinning будет успешно расшифрован.

Успешное соединение с HTTP ToolkitУспешное соединение с HTTP Toolkit

К счастью, приложение оказалось как раз из таких немного побродив по приложению и открыв дверь, можно переходить к анализу логов.

Запрос открытия двериЗапрос открытия двери

Всего интересными показалось три запроса:

  1. Запрос на открытие двери подъезда: POST по адресу /rest/v1/places/{place_id}/accesscontrols/{control_id}/actions с JSON-телом {"name": "accessControlOpen"}

  2. Получение снимка (превью) с камеры: GET по адресу /rest/v1/places/{place_id}/accesscontrols/{control_id}/videosnapshots

  3. Получение ссылки на видеопоток с аудио: GET по адресу /rest/v1/forpost/cameras/{camera_id}/video?LightStream=0

HTTP заголовки всех трёх запросов содержат ключ Authorization судя по всему, именно по нему происходит авторизация при выполнении запросов. Сделав пару запросов руками через Advanced REST Client, чтобы убедиться, что заголовка Authorization достаточно и в самостоятельной работе с API не осталось подводных камней, я понял, что можно откладывать эмулятор и переходить к написанию кода.

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

HEADERS = {"Authorization": "Bearer ###"}ACTION_URL = "https://###.ru/rest/v1/places/###/accesscontrols/###/"VIDEO_URL = "https://###.ru/rest/v1/forpost/cameras/###/video?LightStream=0"def get_image():    result = requests.get(f'{ACTION_URL}/videosnapshots', headers=HEADERS)    if result.status_code != 200:        logging.error(f"Failed to get an image with status code {result.status_code}")        return None    logging.warning(f"Image received successfully in {result.elapsed.total_seconds()}sec")    return result.contentdef open_door():    result = requests.post(        f'{ACTION_URL}/actions', headers=HEADERS, json={"name": "accessControlOpen"})    if result.status_code != 200:        logging.error(f"Failed to open the door with status code {result.status_code}")        return False    logging.warning(f"Door opened successfully in {result.elapsed.total_seconds()}sec")    return Truedef get_videostream_link():    result = requests.get(VIDEO_URL, headers=HEADERS)    if result.status_code != 200:        logging.error(f"Failed to get stream link with status code {result.status_code}")        return False    logging.warning(f"Stream link received successfully in {result.elapsed.total_seconds()}sec")    return result.json()['data']['URL']

Поиск знакомых лиц в кадре

Прежде чем начать этот раздел, нужно рассказать пару слов об уже имеющихся на тот момент у меня в распоряжении серверных мощностях это недорогая виртуальная машина с доступом ко всего одному потоку Intel(R) Xeon(R) CPU E5-2650L v3 @ 1.80GHz, 1GB оперативной памяти и 0 GPU. Тратиться на более дорогую конфигурацию не хотелось, а значит, нужно было попробовать выжать максимум из имеющихся ресурсов.

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

Непродолжительный поиск существующих решений привёл на страницу Interactive Face Recognition Demo официального демо, показывающего ровно необходимый функционал сравнения видимых в кадре лиц с базой заранее сохранённых. Единственная проблема состояла в том, что данный пример по каким-то причинам исчез после релиза 2020.3, а удобная установка пакета через pip у проекта появилась только с 2021.1. Было решено установить последнюю версию OpenVINO и адаптировать код под неё.

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

class ImageProcessor:    def __init__(self):        self.frame_processor = FrameProcessor()    def process(self, image):        detections = self.frame_processor.process(image)        labels = []        for roi, landmarks, identity in zip(*detections):            label = self.frame_processor.face_identifier.get_identity_label(                identity.id)            labels.append(label)        return labels

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

100 runs on an image with known face:Total time: 7.356sTime per frame: 0.007sFPS: 135.944100 runs on an image without faces:Total time: 2.985sTime per frame: 0.003sFPS: 334.962

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

1 FPS: Работа со снимками с камеры

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

class ImageProcessor:# <...>    def process_single_image(self, image):        nparr = np.fromstring(image, np.uint8)        img_np = cv2.imdecode(nparr, cv2.IMREAD_COLOR)        labels = self.process(img_np)        return labelsdef snapshot_based_intercom_id():    processor = ImageProcessor()    last_open_door_time = time.time()    while True:        start_time = time.time()        image = get_image()        result = processor.process_single_image(image)        logging.info(f'{result} in {time.time() - start_time}s')        # Successfull detections are "face{N}"        if any(['face' in res for res in result]):            if start_time - last_open_door_time > 5:                open_door()                with open(f'images/{start_time}_OK.jfif', 'wb') as f:                    f.write(image)                last_open_door_time = start_time

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

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

Заработало! Полностью довольный первым запуском, я вернулся в квартиру. Единственное, что портило впечатление от системы распознавания время реакции на появление лица в кадре, т.к. время отклика API оставляло желать лучшего. Низкая частота поступления данных, 0.7с на получение картинки и 0.6с на открытие двери, давали видимый невооружённым взглядом лаг.

До 30 FPS: Обработка видеопотока

Получить кадры из видео оказалось на удивление просто:

vcap = cv2.VideoCapture(link)success, frame = vcap.read()

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

Первым потенциальным решением было установить размер внутренней очереди: vcap.set(CV_CAP_PROP_BUFFERSIZE, 0);. Согласно найденной информации, такой трюк должен был хорошо работать с любой конфигурацией системы для версий OpenCV выше 3.4, но по какой-то причине, так и не оказал никакого влияния в моём случае. Единственной рабочей альтернативой стал подход, описанный в этом ответе со StackOverflow завести отдельный поток, читающий кадры из камеры на максимально возможной скорости и сохраняющий последний в поле класса для дальнейшего доступа (впоследствии оказалось, что именно этот цикл ответственен за большую часть потребления процессора).

Получилась модификация ImageProcessor для обработки видеопотока с частотой 3 кадра в секунду:

class CameraBufferCleanerThread(threading.Thread):    def __init__(self, camera, name='camera-buffer-cleaner-thread'):        self.camera = camera        self.last_frame = None        self.finished = False        super(CameraBufferCleanerThread, self).__init__(name=name)        self.start()    def run(self):        while not self.finished:            ret, self.last_frame = self.camera.read()    def __enter__(self): return self    def __exit__(self, type, value, traceback):        self.finished = True        self.join()class ImageProcessor:# <...>    def process_stream(self, link):        vcap = cv2.VideoCapture(link)        interval = 0.3 # ~3 FPS        with CameraBufferCleanerThread(vcap) as cam_cleaner:            while True:                frame = cam_cleaner.last_frame                if frame is not None:                    yield (self.process(frame), frame)                else:                    yield (None, None)                time.sleep(interval)

И соответствующая модификация snapshot_based_intercom_id:

def stream_based_intercom_id():    processor = ImageProcessor()    link = get_videostream_link()    # To notify about delays    last_time = time.time()    last_open_door_time = time.time()    for result, np_image in processor.process_stream(link):        current_time = time.time()        delta_time = current_time - last_time        if delta_time < 1:            logging.info(f'{result} in {delta_time}')        else:            logging.warning(f'{result} in {delta_time}')        last_time = current_time        if result is None:            continue        if any(['face' in res for res in result]):            if current_time - last_open_door_time > 5:                logging.warning(                  f'Hey, I know you - {result[0]}! Opening the door...')                last_open_door_time = current_time                open_door()                cv2.imwrite(f'images/{current_time}_OK.jpg', np_image)

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

Момент успешного распознавания, версия с обработкой видеопотокаМомент успешного распознавания, версия с обработкой видеопотока

Управление с помощью Telegram бота

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

С помощью пакета python-telegram-bot была написана простая оболочка, принимающая в себя callback включения/выключения системы и список доверенных никнеймов.

class TelegramInterface:    def __init__(self, login_whitelist, state_callback):        self.state_callback = state_callback        self.login_whitelist = login_whitelist        self.updater = Updater(            token = "###", use_context = True)        self.run()    def run(self):        dispatcher = self.updater.dispatcher        dispatcher.add_handler(CommandHandler("start", self.start))        dispatcher.add_handler(CommandHandler("run", self.run_intercom))        dispatcher.add_handler(CommandHandler("stop", self.stop_intercom))        self.updater.start_polling()    def run_intercom(self, update: Update, context: CallbackContext):        user = update.message.from_user        update.message.reply_text(            self.state_callback(True) if user.username in self.login_whitelist else 'not allowed',            reply_to_message_id=update.message.message_id)    def stop_intercom(self, update: Update, context: CallbackContext):        user = update.message.from_user        update.message.reply_text(            self.state_callback(False) if user.username in self.login_whitelist else 'not allowed',            reply_to_message_id=update.message.message_id)    def start(self, update: Update, context: CallbackContext) -> None:        update.message.reply_text('Hi!')                class TelegramBotThreadWrapper(threading.Thread):    def __init__(self, state_callback, name='telegram-bot-wrapper'):        self.whitelist = ["###", "###"]        self.state_callback = state_callback        super(TelegramBotThreadWrapper, self).__init__(name=name)        self.start()    def run(self):        self.bot = TelegramInterface(self.whitelist, self.state_callback)

И следующая ступень эволюции функции intercom_id, обрабатывающая синхронизацию между потоками обработки данных и получения команд от бота:

def stream_based_intercom_id_with_telegram():    processor = ImageProcessor()    loop_state_lock = threading.Lock()    loop_should_run = False    loop_should_change_state_cv = threading.Condition(loop_state_lock)    is_loop_finished = True    loop_changed_state_cv = threading.Condition(loop_state_lock)    def stream_processing_loop():        nonlocal loop_should_run        nonlocal loop_should_change_state_cv        nonlocal is_loop_finished        nonlocal loop_changed_state_cv        while True:            with loop_should_change_state_cv:                loop_should_change_state_cv.wait_for(lambda: loop_should_run)                is_loop_finished = False                loop_changed_state_cv.notify_all()                logging.warning(f'Loop is started')            link = get_videostream_link()            last_time = time.time()            last_open_door_time = time.time()            for result, np_image in processor.process_stream(link):                with loop_should_change_state_cv:                    if not loop_should_run:                        is_loop_finished = True                        loop_changed_state_cv.notify_all()                        logging.warning(f'Loop is stopped')                        break                current_time = time.time()                delta_time = current_time - last_time                if delta_time < 1:                    logging.info(f'{result} in {delta_time}')                else:                    logging.warning(f'{result} in {delta_time}')                last_time = current_time                if result is None:                    continue                if any(['face' in res for res in result]):                    if current_time - last_open_door_time > 5:                        logging.warning(f'Hey, I know you - {result[0]}! Opening the door...')                        last_open_door_time = current_time                        open_door()                        cv2.imwrite(f'images/{current_time}_OK.jpg', np_image)    def state_callback(is_running):        nonlocal loop_should_run        nonlocal loop_should_change_state_cv        nonlocal is_loop_finished        nonlocal loop_changed_state_cv        with loop_should_change_state_cv:            if is_running == loop_should_run:                return "Intercom service state is not changed"            loop_should_run = is_running            if loop_should_run:                loop_should_change_state_cv.notify_all()                loop_changed_state_cv.wait_for(lambda: not is_loop_finished)                return "Intercom service is up"            else:                loop_changed_state_cv.wait_for(lambda: is_loop_finished)                return "Intercom service is down"    telegram_bot = TelegramBotThreadWrapper(state_callback)    logging.warning("Bot is ready")    stream_processing_loop()

Результат

Видео:

Послесловие

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

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

Подробнее..

Перевод Обнаружение эмоций на лице в браузере с помощью глубокого обучения и TensorFlow.js. Часть 2

02.03.2021 20:22:33 | Автор: admin

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

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

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


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

Настройка по данным об эмоциях на лице FER2013

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

Давайте изменим окончательный код из проекта отслеживания лиц, чтобы обучить нейросетевую модель и применить её к данным о лицах. Набор данных FER2013 состоит более чем из 28 тысяч помеченных изображений лиц; он доступен на веб-сайте Kaggle. Мы загрузили эту версию, в которой набор данных уже преобразован в файлы изображений, и поместили её в папку web/fer2013. Затем мы обновили код сервера NodeJS в index.js, чтобы он возвращал список ссылок на изображения по адресу http://localhost:8080/data/. Поэтому вы можете получить полный объект JSON, если запустите сервер локально.

Чтобы упростить задачу, мы сохранили этот объект JSON в файле web/fer2013.js, чтобы вы могли использовать его напрямую, не запуская сервер локально. Вы можете включить его в другие файлы скриптов в верхней части страницы:

<script src="web/fer2013.js"></script>

Мы собираемся работать с изображениями, а не с видео с веб-камеры (не беспокойтесь, мы вернёмся к видео в следующей статье). Поэтому нам нужно заменить элемент<video> элементом <img>и переименовать его ID в image. Мы также можем удалить функцию setupWebcam, так как для этого проекта она не нужна.

<img id="image" style="    visibility: hidden;    width: auto;    height: auto;    "/>

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

async function setImage( url ) {    return new Promise( res => {        let image = document.getElementById( "image" );        image.src = url;        image.onload = () => {            res();        };    });}function shuffleArray( array ) {    for( let i = array.length - 1; i > 0; i-- ) {        const j = Math.floor( Math.random() * ( i + 1 ) );        [ array[ i ], array[ j ] ] = [ array[ j ], array[ i ] ];    }}const OUTPUT_SIZE = 500;

Нам понадобятся некоторые глобальные переменные: для списка категорий эмоций, списка агрегированных массивов данных FER и индекса массива:

const emotions = [ "angry", "disgust", "fear", "happy", "neutral", "sad", "surprise" ];let ferData = [];let setIndex = 0;

Внутри блока async мы можем подготовить и перетасовать данные FER и изменить размер элемента canvas до 500x500 пикселей:

const minSamples = Math.min( ...Object.keys( fer2013 ).map( em => fer2013[ em ].length ) );Object.keys( fer2013 ).forEach( em => {    shuffleArray( fer2013[ em ] );    for( let i = 0; i < minSamples; i++ ) {        ferData.push({            emotion: em,            file: fer2013[ em ][ i ]        });    }});shuffleArray( ferData );let canvas = document.getElementById( "output" );canvas.width = OUTPUT_SIZE;canvas.height = OUTPUT_SIZE;

Нам нужно в последний раз обновить шаблон кода перед обучением модели ИИ на одной странице и применением обученной модели на второй странице. Необходимо обновить функцию trackFace, чтобы она работала с элементом image, а не video. Также требуется масштабировать ограничивающий прямоугольник и выходные данные сетки для лица в соответствии с размером элемента canvas. Мы зададим приращение setIndex в конце функции для перехода к следующему изображению.

async function trackFace() {    // Set to the next training image    await setImage( ferData[ setIndex ].file );    const image = document.getElementById( "image" );    const faces = await model.estimateFaces( {        input: image,        returnTensors: false,        flipHorizontal: false,    });    output.drawImage(        image,        0, 0, image.width, image.height,        0, 0, OUTPUT_SIZE, OUTPUT_SIZE    );    const scale = OUTPUT_SIZE / image.width;    faces.forEach( face => {        // Draw the bounding box        const x1 = face.boundingBox.topLeft[ 0 ];        const y1 = face.boundingBox.topLeft[ 1 ];        const x2 = face.boundingBox.bottomRight[ 0 ];        const y2 = face.boundingBox.bottomRight[ 1 ];        const bWidth = x2 - x1;        const bHeight = y2 - y1;        drawLine( output, x1, y1, x2, y1, scale );        drawLine( output, x2, y1, x2, y2, scale );        drawLine( output, x1, y2, x2, y2, scale );        drawLine( output, x1, y1, x1, y2, scale );        // Draw the face mesh        const keypoints = face.scaledMesh;        for( let i = 0; i < FaceTriangles.length / 3; i++ ) {            let pointA = keypoints[ FaceTriangles[ i * 3 ] ];            let pointB = keypoints[ FaceTriangles[ i * 3 + 1 ] ];            let pointC = keypoints[ FaceTriangles[ i * 3 + 2 ] ];            drawTriangle( output, pointA[ 0 ], pointA[ 1 ], pointB[ 0 ], pointB[ 1 ], pointC[ 0 ], pointC[ 1 ], scale );        }    });    setText( `${setIndex + 1}. Face Tracking Confidence: ${face.faceInViewConfidence.toFixed( 3 )} - ${ferData[ setIndex ].emotion}` );    setIndex++;    requestAnimationFrame( trackFace );}

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

1. Глубокое изучение эмоций на лице

В этом первом файле веб-страницы мы собираемся задать обучающие данные, создать нейросетевую модель, а затем обучить её и сохранить веса в файл. В код включена предварительно обученная модель (см. папку web/model), поэтому при желании можно пропустить эту часть и перейти к части2.

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

let trainingData = [];function emotionToArray( emotion ) {    let array = [];    for( let i = 0; i < emotions.length; i++ ) {        array.push( emotion === emotions[ i ] ? 1 : 0 );    }    return array;}

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

// Add just the nose, cheeks, eyes, eyebrows & mouthconst features = [    "noseTip",    "leftCheek",    "rightCheek",    "leftEyeLower1", "leftEyeUpper1",    "rightEyeLower1", "rightEyeUpper1",    "leftEyebrowLower", //"leftEyebrowUpper",    "rightEyebrowLower", //"rightEyebrowUpper",    "lipsLowerInner", //"lipsLowerOuter",    "lipsUpperInner", //"lipsUpperOuter",];let points = [];features.forEach( feature => {    face.annotations[ feature ].forEach( x => {        points.push( ( x[ 0 ] - x1 ) / bWidth );        points.push( ( x[ 1 ] - y1 ) / bHeight );    });});// Only grab the faces that are confidentif( face.faceInViewConfidence > 0.9 ) {    trainingData.push({        input: points,        output: ferData[ setIndex ].emotion,    });}

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

async function trackFace() {    // Fast train on just 200 of the images    if( setIndex >= 200 ) {        setText( "Finished!" );        trainNet();        return;    }    ...}

Наконец, мы пришли к той части, которую так долго ждали: давайте создадим функцию trainNet и обучим нашу модель ИИ!

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

async function trainNet() {    let inputs = trainingData.map( x => x.input );    let outputs = trainingData.map( x => emotionToArray( x.output ) );    // Define our model with several hidden layers    const model = tf.sequential();    model.add(tf.layers.dense( { units: 100, activation: "relu", inputShape: [ inputs[ 0 ].length ] } ) );    model.add(tf.layers.dense( { units: 100, activation: "relu" } ) );    model.add(tf.layers.dense( { units: 100, activation: "relu" } ) );    model.add(tf.layers.dense( {        units: emotions.length,        kernelInitializer: 'varianceScaling',        useBias: false,        activation: "softmax"    } ) );    model.compile({        optimizer: "adam",        loss: "categoricalCrossentropy",        metrics: "acc"    });    const xs = tf.stack( inputs.map( x => tf.tensor1d( x ) ) );    const ys = tf.stack( outputs.map( x => tf.tensor1d( x ) ) );    await model.fit( xs, ys, {        epochs: 1000,        shuffle: true,        callbacks: {            onEpochEnd: ( epoch, logs ) => {                setText( `Training... Epoch #${epoch} (${logs.acc.toFixed( 3 )})` );                console.log( "Epoch #", epoch, logs );            }        }    } );    // Download the trained model    const saveResult = await model.save( "downloads://facemo" );}

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

1. Финишная прямая

Вот полный код обучения модели на наборе данных FER:
<html>    <head>        <title>Training - Recognizing Facial Expressions in the Browser with Deep Learning using TensorFlow.js</title>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.4.0/dist/tf.min.js"></script>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow-models/face-landmarks-detection@0.0.1/dist/face-landmarks-detection.js"></script>        <script src="web/triangles.js"></script>        <script src="web/fer2013.js"></script>    </head>    <body>        <canvas id="output"></canvas>        <img id="image" style="            visibility: hidden;            width: auto;            height: auto;            "/>        <h1 id="status">Loading...</h1>        <script>        function setText( text ) {            document.getElementById( "status" ).innerText = text;        }        async function setImage( url ) {            return new Promise( res => {                let image = document.getElementById( "image" );                image.src = url;                image.onload = () => {                    res();                };            });        }        function shuffleArray( array ) {            for( let i = array.length - 1; i > 0; i-- ) {                const j = Math.floor( Math.random() * ( i + 1 ) );                [ array[ i ], array[ j ] ] = [ array[ j ], array[ i ] ];            }        }        function drawLine( ctx, x1, y1, x2, y2, scale = 1 ) {            ctx.beginPath();            ctx.moveTo( x1 * scale, y1 * scale );            ctx.lineTo( x2 * scale, y2 * scale );            ctx.stroke();        }        function drawTriangle( ctx, x1, y1, x2, y2, x3, y3, scale = 1 ) {            ctx.beginPath();            ctx.moveTo( x1 * scale, y1 * scale );            ctx.lineTo( x2 * scale, y2 * scale );            ctx.lineTo( x3 * scale, y3 * scale );            ctx.lineTo( x1 * scale, y1 * scale );            ctx.stroke();        }        const OUTPUT_SIZE = 500;        const emotions = [ "angry", "disgust", "fear", "happy", "neutral", "sad", "surprise" ];        let ferData = [];        let setIndex = 0;        let trainingData = [];        let output = null;        let model = null;        function emotionToArray( emotion ) {            let array = [];            for( let i = 0; i < emotions.length; i++ ) {                array.push( emotion === emotions[ i ] ? 1 : 0 );            }            return array;        }        async function trainNet() {            let inputs = trainingData.map( x => x.input );            let outputs = trainingData.map( x => emotionToArray( x.output ) );            // Define our model with several hidden layers            const model = tf.sequential();            model.add(tf.layers.dense( { units: 100, activation: "relu", inputShape: [ inputs[ 0 ].length ] } ) );            model.add(tf.layers.dense( { units: 100, activation: "relu" } ) );            model.add(tf.layers.dense( { units: 100, activation: "relu" } ) );            model.add(tf.layers.dense( {                units: emotions.length,                kernelInitializer: 'varianceScaling',                useBias: false,                activation: "softmax"            } ) );            model.compile({                optimizer: "adam",                loss: "categoricalCrossentropy",                metrics: "acc"            });            const xs = tf.stack( inputs.map( x => tf.tensor1d( x ) ) );            const ys = tf.stack( outputs.map( x => tf.tensor1d( x ) ) );            await model.fit( xs, ys, {                epochs: 1000,                shuffle: true,                callbacks: {                    onEpochEnd: ( epoch, logs ) => {                        setText( `Training... Epoch #${epoch} (${logs.acc.toFixed( 3 )})` );                        console.log( "Epoch #", epoch, logs );                    }                }            } );            // Download the trained model            const saveResult = await model.save( "downloads://facemo" );        }        async function trackFace() {            // Fast train on just 200 of the images            if( setIndex >= 200 ) {//ferData.length ) {                setText( "Finished!" );                trainNet();                return;            }            // Set to the next training image            await setImage( ferData[ setIndex ].file );            const image = document.getElementById( "image" );            const faces = await model.estimateFaces( {                input: image,                returnTensors: false,                flipHorizontal: false,            });            output.drawImage(                image,                0, 0, image.width, image.height,                0, 0, OUTPUT_SIZE, OUTPUT_SIZE            );            const scale = OUTPUT_SIZE / image.width;            faces.forEach( face => {                // Draw the bounding box                const x1 = face.boundingBox.topLeft[ 0 ];                const y1 = face.boundingBox.topLeft[ 1 ];                const x2 = face.boundingBox.bottomRight[ 0 ];                const y2 = face.boundingBox.bottomRight[ 1 ];                const bWidth = x2 - x1;                const bHeight = y2 - y1;                drawLine( output, x1, y1, x2, y1, scale );                drawLine( output, x2, y1, x2, y2, scale );                drawLine( output, x1, y2, x2, y2, scale );                drawLine( output, x1, y1, x1, y2, scale );                // Draw the face mesh                const keypoints = face.scaledMesh;                for( let i = 0; i < FaceTriangles.length / 3; i++ ) {                    let pointA = keypoints[ FaceTriangles[ i * 3 ] ];                    let pointB = keypoints[ FaceTriangles[ i * 3 + 1 ] ];                    let pointC = keypoints[ FaceTriangles[ i * 3 + 2 ] ];                    drawTriangle( output, pointA[ 0 ], pointA[ 1 ], pointB[ 0 ], pointB[ 1 ], pointC[ 0 ], pointC[ 1 ], scale );                }                // Add just the nose, cheeks, eyes, eyebrows & mouth                const features = [                    "noseTip",                    "leftCheek",                    "rightCheek",                    "leftEyeLower1", "leftEyeUpper1",                    "rightEyeLower1", "rightEyeUpper1",                    "leftEyebrowLower", //"leftEyebrowUpper",                    "rightEyebrowLower", //"rightEyebrowUpper",                    "lipsLowerInner", //"lipsLowerOuter",                    "lipsUpperInner", //"lipsUpperOuter",                ];                let points = [];                features.forEach( feature => {                    face.annotations[ feature ].forEach( x => {                        points.push( ( x[ 0 ] - x1 ) / bWidth );                        points.push( ( x[ 1 ] - y1 ) / bHeight );                    });                });                // Only grab the faces that are confident                if( face.faceInViewConfidence > 0.9 ) {                    trainingData.push({                        input: points,                        output: ferData[ setIndex ].emotion,                    });                }            });            setText( `${setIndex + 1}. Face Tracking Confidence: ${face.faceInViewConfidence.toFixed( 3 )} - ${ferData[ setIndex ].emotion}` );            setIndex++;            requestAnimationFrame( trackFace );        }        (async () => {            // Get FER-2013 data from the local web server            // https://www.kaggle.com/msambare/fer2013            // The data can be downloaded from Kaggle and placed inside the "web/fer2013" folder            // Get the lowest number of samples out of all emotion categories            const minSamples = Math.min( ...Object.keys( fer2013 ).map( em => fer2013[ em ].length ) );            Object.keys( fer2013 ).forEach( em => {                shuffleArray( fer2013[ em ] );                for( let i = 0; i < minSamples; i++ ) {                    ferData.push({                        emotion: em,                        file: fer2013[ em ][ i ]                    });                }            });            shuffleArray( ferData );            let canvas = document.getElementById( "output" );            canvas.width = OUTPUT_SIZE;            canvas.height = OUTPUT_SIZE;            output = canvas.getContext( "2d" );            output.translate( canvas.width, 0 );            output.scale( -1, 1 ); // Mirror cam            output.fillStyle = "#fdffb6";            output.strokeStyle = "#fdffb6";            output.lineWidth = 2;            // Load Face Landmarks Detection            model = await faceLandmarksDetection.load(                faceLandmarksDetection.SupportedPackages.mediapipeFacemesh            );            setText( "Loaded!" );            trackFace();        })();        </script>    </body></html>

2. Обнаружение эмоций на лице

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

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

let emotionModel = null;(async () => {    ...    // Load Face Landmarks Detection    model = await faceLandmarksDetection.load(        faceLandmarksDetection.SupportedPackages.mediapipeFacemesh    );    // Load Emotion Detection    emotionModel = await tf.loadLayersModel( 'web/model/facemo.json' );    ...})();

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

async function predictEmotion( points ) {    let result = tf.tidy( () => {        const xs = tf.stack( [ tf.tensor1d( points ) ] );        return emotionModel.predict( xs );    });    let prediction = await result.data();    result.dispose();    // Get the index of the maximum value    let id = prediction.indexOf( Math.max( ...prediction ) );    return emotions[ id ];}

Чтобы между тестовыми изображениями можно было делать паузу в несколько секунд, давайте создадим служебную функцию wait:

function wait( ms ) {    return new Promise( res => setTimeout( res, ms ) );}

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

async function trackFace() {    ...    let points = null;    faces.forEach( face => {        ...        // Add just the nose, cheeks, eyes, eyebrows & mouth        const features = [            "noseTip",            "leftCheek",            "rightCheek",            "leftEyeLower1", "leftEyeUpper1",            "rightEyeLower1", "rightEyeUpper1",            "leftEyebrowLower", //"leftEyebrowUpper",            "rightEyebrowLower", //"rightEyebrowUpper",            "lipsLowerInner", //"lipsLowerOuter",            "lipsUpperInner", //"lipsUpperOuter",        ];        points = [];        features.forEach( feature => {            face.annotations[ feature ].forEach( x => {                points.push( ( x[ 0 ] - x1 ) / bWidth );                points.push( ( x[ 1 ] - y1 ) / bHeight );            });        });    });    if( points ) {        let emotion = await predictEmotion( points );        setText( `${setIndex + 1}. Expected: ${ferData[ setIndex ].emotion} vs. ${emotion}` );    }    else {        setText( "No Face" );    }    setIndex++;    await wait( 2000 );    requestAnimationFrame( trackFace );}

Готово! Наш код должен начать определять эмоции на изображениях FER в соответствии с ожидаемой эмоцией. Попробуйте, и увидите, как он работает.

2. Финишная прямая

Взгляните на полный код применения обученной модели к изображениям из набора данных FER:
<html>    <head>        <title>Running - Recognizing Facial Expressions in the Browser with Deep Learning using TensorFlow.js</title>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.4.0/dist/tf.min.js"></script>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow-models/face-landmarks-detection@0.0.1/dist/face-landmarks-detection.js"></script>        <script src="web/fer2013.js"></script>    </head>    <body>        <canvas id="output"></canvas>        <img id="image" style="            visibility: hidden;            width: auto;            height: auto;            "/>        <h1 id="status">Loading...</h1>        <script>        function setText( text ) {            document.getElementById( "status" ).innerText = text;        }        async function setImage( url ) {            return new Promise( res => {                let image = document.getElementById( "image" );                image.src = url;                image.onload = () => {                    res();                };            });        }        function shuffleArray( array ) {            for( let i = array.length - 1; i > 0; i-- ) {                const j = Math.floor( Math.random() * ( i + 1 ) );                [ array[ i ], array[ j ] ] = [ array[ j ], array[ i ] ];            }        }        function drawLine( ctx, x1, y1, x2, y2, scale = 1 ) {            ctx.beginPath();            ctx.moveTo( x1 * scale, y1 * scale );            ctx.lineTo( x2 * scale, y2 * scale );            ctx.stroke();        }        function drawTriangle( ctx, x1, y1, x2, y2, x3, y3, scale = 1 ) {            ctx.beginPath();            ctx.moveTo( x1 * scale, y1 * scale );            ctx.lineTo( x2 * scale, y2 * scale );            ctx.lineTo( x3 * scale, y3 * scale );            ctx.lineTo( x1 * scale, y1 * scale );            ctx.stroke();        }        function wait( ms ) {            return new Promise( res => setTimeout( res, ms ) );        }        const OUTPUT_SIZE = 500;        const emotions = [ "angry", "disgust", "fear", "happy", "neutral", "sad", "surprise" ];        let ferData = [];        let setIndex = 0;        let emotionModel = null;        let output = null;        let model = null;        async function predictEmotion( points ) {            let result = tf.tidy( () => {                const xs = tf.stack( [ tf.tensor1d( points ) ] );                return emotionModel.predict( xs );            });            let prediction = await result.data();            result.dispose();            // Get the index of the maximum value            let id = prediction.indexOf( Math.max( ...prediction ) );            return emotions[ id ];        }        async function trackFace() {            // Set to the next training image            await setImage( ferData[ setIndex ].file );            const image = document.getElementById( "image" );            const faces = await model.estimateFaces( {                input: image,                returnTensors: false,                flipHorizontal: false,            });            output.drawImage(                image,                0, 0, image.width, image.height,                0, 0, OUTPUT_SIZE, OUTPUT_SIZE            );            const scale = OUTPUT_SIZE / image.width;            let points = null;            faces.forEach( face => {                // Draw the bounding box                const x1 = face.boundingBox.topLeft[ 0 ];                const y1 = face.boundingBox.topLeft[ 1 ];                const x2 = face.boundingBox.bottomRight[ 0 ];                const y2 = face.boundingBox.bottomRight[ 1 ];                const bWidth = x2 - x1;                const bHeight = y2 - y1;                drawLine( output, x1, y1, x2, y1, scale );                drawLine( output, x2, y1, x2, y2, scale );                drawLine( output, x1, y2, x2, y2, scale );                drawLine( output, x1, y1, x1, y2, scale );                // Add just the nose, cheeks, eyes, eyebrows & mouth                const features = [                    "noseTip",                    "leftCheek",                    "rightCheek",                    "leftEyeLower1", "leftEyeUpper1",                    "rightEyeLower1", "rightEyeUpper1",                    "leftEyebrowLower", //"leftEyebrowUpper",                    "rightEyebrowLower", //"rightEyebrowUpper",                    "lipsLowerInner", //"lipsLowerOuter",                    "lipsUpperInner", //"lipsUpperOuter",                ];                points = [];                features.forEach( feature => {                    face.annotations[ feature ].forEach( x => {                        points.push( ( x[ 0 ] - x1 ) / bWidth );                        points.push( ( x[ 1 ] - y1 ) / bHeight );                    });                });            });            if( points ) {                let emotion = await predictEmotion( points );                setText( `${setIndex + 1}. Expected: ${ferData[ setIndex ].emotion} vs. ${emotion}` );            }            else {                setText( "No Face" );            }            setIndex++;            await wait( 2000 );            requestAnimationFrame( trackFace );        }        (async () => {            // Get FER-2013 data from the local web server            // https://www.kaggle.com/msambare/fer2013            // The data can be downloaded from Kaggle and placed inside the "web/fer2013" folder            // Get the lowest number of samples out of all emotion categories            const minSamples = Math.min( ...Object.keys( fer2013 ).map( em => fer2013[ em ].length ) );            Object.keys( fer2013 ).forEach( em => {                shuffleArray( fer2013[ em ] );                for( let i = 0; i < minSamples; i++ ) {                    ferData.push({                        emotion: em,                        file: fer2013[ em ][ i ]                    });                }            });            shuffleArray( ferData );            let canvas = document.getElementById( "output" );            canvas.width = OUTPUT_SIZE;            canvas.height = OUTPUT_SIZE;            output = canvas.getContext( "2d" );            output.translate( canvas.width, 0 );            output.scale( -1, 1 ); // Mirror cam            output.fillStyle = "#fdffb6";            output.strokeStyle = "#fdffb6";            output.lineWidth = 2;            // Load Face Landmarks Detection            model = await faceLandmarksDetection.load(                faceLandmarksDetection.SupportedPackages.mediapipeFacemesh            );            // Load Emotion Detection            emotionModel = await tf.loadLayersModel( 'web/model/facemo.json' );            setText( "Loaded!" );            trackFace();        })();        </script>    </body></html>

Что дальше? Позволит ли это определять наши эмоции на лице?

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

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

Узнайте подробности, как получить Level Up по навыкам и зарплате или востребованную профессию с нуля, пройдя онлайн-курсы SkillFactory со скидкой 40% и промокодомHABR, который даст еще +10% скидки на обучение.

Подробнее..

Перевод Обнаружение эмоций на лице в реальном времени с помощью веб-камеры в браузере с использованием TensorFlow.js. Часть 3

03.03.2021 22:15:39 | Автор: admin

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


Вы можете загрузить демоверсию этого проекта. Для обеспечения необходимой производительности может потребоваться включить в веб-браузере поддержку интерфейса WebGL. Вы также можете загрузить код и файлы для этой серии. Предполагается, что вы знакомы с JavaScript и HTML и имеете хотя бы базовое представление о нейронных сетях.

Добавление обнаружения эмоций на лице

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

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

const emotions = [ "angry", "disgust", "fear", "happy", "neutral", "sad", "surprise" ];let emotionModel = null;

Затем мы можем загрузить модель обнаружения эмоций внутри блока async:

(async () => {    ...    // Load Face Landmarks Detection    model = await faceLandmarksDetection.load(        faceLandmarksDetection.SupportedPackages.mediapipeFacemesh    );    // Load Emotion Detection    emotionModel = await tf.loadLayersModel( 'web/model/facemo.json' );    ...})();

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

async function predictEmotion( points ) {    let result = tf.tidy( () => {        const xs = tf.stack( [ tf.tensor1d( points ) ] );        return emotionModel.predict( xs );    });    let prediction = await result.data();    result.dispose();    // Get the index of the maximum value    let id = prediction.indexOf( Math.max( ...prediction ) );    return emotions[ id ];}

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

async function trackFace() {    ...    let points = null;    faces.forEach( face => {        ...        // Add just the nose, cheeks, eyes, eyebrows & mouth        const features = [            "noseTip",            "leftCheek",            "rightCheek",            "leftEyeLower1", "leftEyeUpper1",            "rightEyeLower1", "rightEyeUpper1",            "leftEyebrowLower", //"leftEyebrowUpper",            "rightEyebrowLower", //"rightEyebrowUpper",            "lipsLowerInner", //"lipsLowerOuter",            "lipsUpperInner", //"lipsUpperOuter",        ];        points = [];        features.forEach( feature => {            face.annotations[ feature ].forEach( x => {                points.push( ( x[ 0 ] - x1 ) / bWidth );                points.push( ( x[ 1 ] - y1 ) / bHeight );            });        });    });    if( points ) {        let emotion = await predictEmotion( points );        setText( `Detected: ${emotion}` );    }    else {        setText( "No Face" );    }    requestAnimationFrame( trackFace );}

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

Вот полный код, нужный для завершения этого проекта
<html>    <head>        <title>Real-Time Facial Emotion Detection</title>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.4.0/dist/tf.min.js"></script>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow-models/face-landmarks-detection@0.0.1/dist/face-landmarks-detection.js"></script>    </head>    <body>        <canvas id="output"></canvas>        <video id="webcam" playsinline style="            visibility: hidden;            width: auto;            height: auto;            ">        </video>        <h1 id="status">Loading...</h1>        <script>        function setText( text ) {            document.getElementById( "status" ).innerText = text;        }        function drawLine( ctx, x1, y1, x2, y2 ) {            ctx.beginPath();            ctx.moveTo( x1, y1 );            ctx.lineTo( x2, y2 );            ctx.stroke();        }        async function setupWebcam() {            return new Promise( ( resolve, reject ) => {                const webcamElement = document.getElementById( "webcam" );                const navigatorAny = navigator;                navigator.getUserMedia = navigator.getUserMedia ||                navigatorAny.webkitGetUserMedia || navigatorAny.mozGetUserMedia ||                navigatorAny.msGetUserMedia;                if( navigator.getUserMedia ) {                    navigator.getUserMedia( { video: true },                        stream => {                            webcamElement.srcObject = stream;                            webcamElement.addEventListener( "loadeddata", resolve, false );                        },                    error => reject());                }                else {                    reject();                }            });        }        const emotions = [ "angry", "disgust", "fear", "happy", "neutral", "sad", "surprise" ];        let emotionModel = null;        let output = null;        let model = null;        async function predictEmotion( points ) {            let result = tf.tidy( () => {                const xs = tf.stack( [ tf.tensor1d( points ) ] );                return emotionModel.predict( xs );            });            let prediction = await result.data();            result.dispose();            // Get the index of the maximum value            let id = prediction.indexOf( Math.max( ...prediction ) );            return emotions[ id ];        }        async function trackFace() {            const video = document.querySelector( "video" );            const faces = await model.estimateFaces( {                input: video,                returnTensors: false,                flipHorizontal: false,            });            output.drawImage(                video,                0, 0, video.width, video.height,                0, 0, video.width, video.height            );            let points = null;            faces.forEach( face => {                // Draw the bounding box                const x1 = face.boundingBox.topLeft[ 0 ];                const y1 = face.boundingBox.topLeft[ 1 ];                const x2 = face.boundingBox.bottomRight[ 0 ];                const y2 = face.boundingBox.bottomRight[ 1 ];                const bWidth = x2 - x1;                const bHeight = y2 - y1;                drawLine( output, x1, y1, x2, y1 );                drawLine( output, x2, y1, x2, y2 );                drawLine( output, x1, y2, x2, y2 );                drawLine( output, x1, y1, x1, y2 );                // Add just the nose, cheeks, eyes, eyebrows & mouth                const features = [                    "noseTip",                    "leftCheek",                    "rightCheek",                    "leftEyeLower1", "leftEyeUpper1",                    "rightEyeLower1", "rightEyeUpper1",                    "leftEyebrowLower", //"leftEyebrowUpper",                    "rightEyebrowLower", //"rightEyebrowUpper",                    "lipsLowerInner", //"lipsLowerOuter",                    "lipsUpperInner", //"lipsUpperOuter",                ];                points = [];                features.forEach( feature => {                    face.annotations[ feature ].forEach( x => {                        points.push( ( x[ 0 ] - x1 ) / bWidth );                        points.push( ( x[ 1 ] - y1 ) / bHeight );                    });                });            });            if( points ) {                let emotion = await predictEmotion( points );                setText( `Detected: ${emotion}` );            }            else {                setText( "No Face" );            }            requestAnimationFrame( trackFace );        }        (async () => {            await setupWebcam();            const video = document.getElementById( "webcam" );            video.play();            let videoWidth = video.videoWidth;            let videoHeight = video.videoHeight;            video.width = videoWidth;            video.height = videoHeight;            let canvas = document.getElementById( "output" );            canvas.width = video.width;            canvas.height = video.height;            output = canvas.getContext( "2d" );            output.translate( canvas.width, 0 );            output.scale( -1, 1 ); // Mirror cam            output.fillStyle = "#fdffb6";            output.strokeStyle = "#fdffb6";            output.lineWidth = 2;            // Load Face Landmarks Detection            model = await faceLandmarksDetection.load(                faceLandmarksDetection.SupportedPackages.mediapipeFacemesh            );            // Load Emotion Detection            emotionModel = await tf.loadLayersModel( 'web/model/facemo.json' );            setText( "Loaded!" );            trackFace();        })();        </script>    </body></html>

Что дальше? Когда мы сможем носить виртуальные очки?

Взяв код из первых двух статей этой серии, мы смогли создать детектор эмоций на лице в реальном времени, используя лишь немного кода на JavaScript. Только представьте, что ещё можно сделать с помощью библиотеки TensorFlow.js! В следующей статье мы вернёмся к нашей цели создать фильтр для лица в стиле Snapchat, используя то, что мы уже узнали об отслеживании лиц и добавлении 3D-визуализации посредством ThreeJS. Оставайтесь с нами! До встречи завтра, в это же время!

Отслеживание лиц в реальном времени в браузере с использованием TensorFlow.js. Часть 2

Узнайте подробности, как получить Level Up по навыкам и зарплате или востребованную профессию с нуля, пройдя онлайн-курсы SkillFactory со скидкой 40% и промокодомHABR, который даст еще +10% скидки на обучение.

Подробнее..

Перевод Отслеживание лиц в реальном времени в браузере с использованием TensorFlow.js. Часть 4

04.03.2021 20:15:20 | Автор: admin

В 4 части (вы же прочли первую, вторую и третью, да?) мы возвращаемся к нашей цели создание фильтра для лица в стиле Snapchat, используя то, что мы уже узнали об отслеживании лиц и добавлении 3D-визуализации посредством ThreeJS. В этой статье мы собираемся использовать ключевые точки лица для виртуальной визуализации 3D-модели поверх видео с веб-камеры, чтобы немного развлечься с дополненной реальностью.


Вы можете загрузить демоверсию этого проекта. Для обеспечения необходимой производительности может потребоваться включить в веб-браузере поддержку интерфейса WebGL. Вы также можете загрузить код и файлы для этой серии. Предполагается, что вы знакомы с JavaScript и HTML и имеете хотя бы базовое представление о нейронных сетях.

Добавление 3D-графики с помощью ThreeJS

Этот проект будет основан на коде проекта отслеживания лиц, который мы создали в начале этой серии. Мы добавим наложение 3D-сцены на исходное полотно.

ThreeJS позволяет относительно легко работать с 3D-графикой, поэтому мы собираемся с помощью этой библиотеки визуализировать виртуальные очки поверх наших лиц.

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

<script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/three@0.123.0/build/three.min.js"></script><script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/three@0.123.0/examples/js/loaders/GLTFLoader.js"></script>

Чтобы упростить задачу и не беспокоиться о том, как поместить текстуру веб-камеры на сцену, мы можем наложить дополнительное прозрачное полотно (canvas) и нарисовать виртуальные очки на нём. Мы используем CSS-код, приведённый ниже над тегом body, поместив выходное полотно (output) в контейнер и добавив полотно наложения (overlay).

<style>    .canvas-container {        position: relative;        width: auto;        height: auto;    }    .canvas-container canvas {        position: absolute;        left: 0;        width: auto;        height: auto;    }</style><body>    <div class="canvas-container">        <canvas id="output"></canvas>        <canvas id="overlay"></canvas>    </div>    ...</body>

Для 3D-сцены требуется несколько переменных, и мы можем добавить служебную функцию загрузки 3D-модели для файлов GLTF:

<style>    .canvas-container {        position: relative;        width: auto;        height: auto;    }    .canvas-container canvas {        position: absolute;        left: 0;        width: auto;        height: auto;    }</style><body>    <div class="canvas-container">        <canvas id="output"></canvas>        <canvas id="overlay"></canvas>    </div>    ...</body>

Теперь мы можем инициализировать все компоненты нашего блока async, начиная с размера полотна наложения, как это было сделано с выходным полотном:

(async () => {    ...    let canvas = document.getElementById( "output" );    canvas.width = video.width;    canvas.height = video.height;    let overlay = document.getElementById( "overlay" );    overlay.width = video.width;    overlay.height = video.height;    ...})();

Также необходимо задать переменные renderer, scene и camera. Даже если вы не знакомы с трёхмерной перспективой и математикой камеры, вам не надо волноваться. Этот код просто располагает камеру сцены так, чтобы ширина и высота видео веб-камеры соответствовали координатам трёхмерного пространства:

(async () => {    ...    // Load Face Landmarks Detection    model = await faceLandmarksDetection.load(        faceLandmarksDetection.SupportedPackages.mediapipeFacemesh    );    renderer = new THREE.WebGLRenderer({        canvas: document.getElementById( "overlay" ),        alpha: true    });    camera = new THREE.PerspectiveCamera( 45, 1, 0.1, 2000 );    camera.position.x = videoWidth / 2;    camera.position.y = -videoHeight / 2;    camera.position.z = -( videoHeight / 2 ) / Math.tan( 45 / 2 ); // distance to z should be tan( fov / 2 )    scene = new THREE.Scene();    scene.add( new THREE.AmbientLight( 0xcccccc, 0.4 ) );    camera.add( new THREE.PointLight( 0xffffff, 0.8 ) );    scene.add( camera );    camera.lookAt( { x: videoWidth / 2, y: -videoHeight / 2, z: 0, isVector3: true } );    ...})();

Нам нужно добавить в функцию trackFace всего лишь одну строку кода для визуализации сцены поверх выходных данных отслеживания лица:

async function trackFace() {    const video = document.querySelector( "video" );    output.drawImage(        video,        0, 0, video.width, video.height,        0, 0, video.width, video.height    );    renderer.render( scene, camera );    const faces = await model.estimateFaces( {        input: video,        returnTensors: false,        flipHorizontal: false,    });    ...}

Последний этап этого ребуса перед отображением виртуальных объектов на нашем лице загрузка 3D-модели виртуальных очков. Мы нашли пару очков в форме сердца от Maximkuzlin на SketchFab. При желании вы можете загрузить и использовать другой объект.

Здесь показано, как загрузить объект и добавить его в сцену до вызова функции trackFace:

Размещение виртуальных очков на отслеживаемом лице

Теперь начинается самое интересное наденем наши виртуальные очки.

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

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

glasses.position.x = face.annotations.midwayBetweenEyes[ 0 ][ 0 ];glasses.position.y = -face.annotations.midwayBetweenEyes[ 0 ][ 1 ];glasses.position.z = -camera.position.z + face.annotations.midwayBetweenEyes[ 0 ][ 2 ];

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

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

glasses.up.x = face.annotations.midwayBetweenEyes[ 0 ][ 0 ] - face.annotations.noseBottom[ 0 ][ 0 ];glasses.up.y = -( face.annotations.midwayBetweenEyes[ 0 ][ 1 ] - face.annotations.noseBottom[ 0 ][ 1 ] );glasses.up.z = face.annotations.midwayBetweenEyes[ 0 ][ 2 ] - face.annotations.noseBottom[ 0 ][ 2 ];const length = Math.sqrt( glasses.up.x ** 2 + glasses.up.y ** 2 + glasses.up.z ** 2 );glasses.up.x /= length;glasses.up.y /= length;glasses.up.z /= length;

Чтобы получить относительный размер головы, можно вычислить расстояние между глазами:

const eyeDist = Math.sqrt(    ( face.annotations.leftEyeUpper1[ 3 ][ 0 ] - face.annotations.rightEyeUpper1[ 3 ][ 0 ] ) ** 2 +    ( face.annotations.leftEyeUpper1[ 3 ][ 1 ] - face.annotations.rightEyeUpper1[ 3 ][ 1 ] ) ** 2 +    ( face.annotations.leftEyeUpper1[ 3 ][ 2 ] - face.annotations.rightEyeUpper1[ 3 ][ 2 ] ) ** 2);

Наконец, мы масштабируем очки на основе значения eyeDist и ориентируем очки по оси Z, используя угол между вектором вверх и осью Y. И вуаля!

Выполните свой код и проверьте результат.

Прежде чем перейти к следующей части этой серии, давайте посмотрим на полный код, собранный вместе:

Простыня с кодом
<html>    <head>        <title>Creating a Snapchat-Style Virtual Glasses Face Filter</title>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.4.0/dist/tf.min.js"></script>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow-models/face-landmarks-detection@0.0.1/dist/face-landmarks-detection.js"></script>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/three@0.123.0/build/three.min.js"></script>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/three@0.123.0/examples/js/loaders/GLTFLoader.js"></script>    </head>    <style>        .canvas-container {            position: relative;            width: auto;            height: auto;        }        .canvas-container canvas {            position: absolute;            left: 0;            width: auto;            height: auto;        }    </style>    <body>        <div class="canvas-container">            <canvas id="output"></canvas>            <canvas id="overlay"></canvas>        </div>        <video id="webcam" playsinline style="            visibility: hidden;            width: auto;            height: auto;            ">        </video>        <h1 id="status">Loading...</h1>        <script>        function setText( text ) {            document.getElementById( "status" ).innerText = text;        }        function drawLine( ctx, x1, y1, x2, y2 ) {            ctx.beginPath();            ctx.moveTo( x1, y1 );            ctx.lineTo( x2, y2 );            ctx.stroke();        }        async function setupWebcam() {            return new Promise( ( resolve, reject ) => {                const webcamElement = document.getElementById( "webcam" );                const navigatorAny = navigator;                navigator.getUserMedia = navigator.getUserMedia ||                navigatorAny.webkitGetUserMedia || navigatorAny.mozGetUserMedia ||                navigatorAny.msGetUserMedia;                if( navigator.getUserMedia ) {                    navigator.getUserMedia( { video: true },                        stream => {                            webcamElement.srcObject = stream;                            webcamElement.addEventListener( "loadeddata", resolve, false );                        },                    error => reject());                }                else {                    reject();                }            });        }        let output = null;        let model = null;        let renderer = null;        let scene = null;        let camera = null;        let glasses = null;        function loadModel( file ) {            return new Promise( ( res, rej ) => {                const loader = new THREE.GLTFLoader();                loader.load( file, function ( gltf ) {                    res( gltf.scene );                }, undefined, function ( error ) {                    rej( error );                } );            });        }        async function trackFace() {            const video = document.querySelector( "video" );            output.drawImage(                video,                0, 0, video.width, video.height,                0, 0, video.width, video.height            );            renderer.render( scene, camera );            const faces = await model.estimateFaces( {                input: video,                returnTensors: false,                flipHorizontal: false,            });            faces.forEach( face => {                // Draw the bounding box                const x1 = face.boundingBox.topLeft[ 0 ];                const y1 = face.boundingBox.topLeft[ 1 ];                const x2 = face.boundingBox.bottomRight[ 0 ];                const y2 = face.boundingBox.bottomRight[ 1 ];                const bWidth = x2 - x1;                const bHeight = y2 - y1;                drawLine( output, x1, y1, x2, y1 );                drawLine( output, x2, y1, x2, y2 );                drawLine( output, x1, y2, x2, y2 );                drawLine( output, x1, y1, x1, y2 );                glasses.position.x = face.annotations.midwayBetweenEyes[ 0 ][ 0 ];                glasses.position.y = -face.annotations.midwayBetweenEyes[ 0 ][ 1 ];                glasses.position.z = -camera.position.z + face.annotations.midwayBetweenEyes[ 0 ][ 2 ];                // Calculate an Up-Vector using the eyes position and the bottom of the nose                glasses.up.x = face.annotations.midwayBetweenEyes[ 0 ][ 0 ] - face.annotations.noseBottom[ 0 ][ 0 ];                glasses.up.y = -( face.annotations.midwayBetweenEyes[ 0 ][ 1 ] - face.annotations.noseBottom[ 0 ][ 1 ] );                glasses.up.z = face.annotations.midwayBetweenEyes[ 0 ][ 2 ] - face.annotations.noseBottom[ 0 ][ 2 ];                const length = Math.sqrt( glasses.up.x ** 2 + glasses.up.y ** 2 + glasses.up.z ** 2 );                glasses.up.x /= length;                glasses.up.y /= length;                glasses.up.z /= length;                // Scale to the size of the head                const eyeDist = Math.sqrt(                    ( face.annotations.leftEyeUpper1[ 3 ][ 0 ] - face.annotations.rightEyeUpper1[ 3 ][ 0 ] ) ** 2 +                    ( face.annotations.leftEyeUpper1[ 3 ][ 1 ] - face.annotations.rightEyeUpper1[ 3 ][ 1 ] ) ** 2 +                    ( face.annotations.leftEyeUpper1[ 3 ][ 2 ] - face.annotations.rightEyeUpper1[ 3 ][ 2 ] ) ** 2                );                glasses.scale.x = eyeDist / 6;                glasses.scale.y = eyeDist / 6;                glasses.scale.z = eyeDist / 6;                glasses.rotation.y = Math.PI;                glasses.rotation.z = Math.PI / 2 - Math.acos( glasses.up.x );            });            requestAnimationFrame( trackFace );        }        (async () => {            await setupWebcam();            const video = document.getElementById( "webcam" );            video.play();            let videoWidth = video.videoWidth;            let videoHeight = video.videoHeight;            video.width = videoWidth;            video.height = videoHeight;            let canvas = document.getElementById( "output" );            canvas.width = video.width;            canvas.height = video.height;            let overlay = document.getElementById( "overlay" );            overlay.width = video.width;            overlay.height = video.height;            output = canvas.getContext( "2d" );            output.translate( canvas.width, 0 );            output.scale( -1, 1 ); // Mirror cam            output.fillStyle = "#fdffb6";            output.strokeStyle = "#fdffb6";            output.lineWidth = 2;            // Load Face Landmarks Detection            model = await faceLandmarksDetection.load(                faceLandmarksDetection.SupportedPackages.mediapipeFacemesh            );            renderer = new THREE.WebGLRenderer({                canvas: document.getElementById( "overlay" ),                alpha: true            });            camera = new THREE.PerspectiveCamera( 45, 1, 0.1, 2000 );            camera.position.x = videoWidth / 2;            camera.position.y = -videoHeight / 2;            camera.position.z = -( videoHeight / 2 ) / Math.tan( 45 / 2 ); // distance to z should be tan( fov / 2 )            scene = new THREE.Scene();            scene.add( new THREE.AmbientLight( 0xcccccc, 0.4 ) );            camera.add( new THREE.PointLight( 0xffffff, 0.8 ) );            scene.add( camera );            camera.lookAt( { x: videoWidth / 2, y: -videoHeight / 2, z: 0, isVector3: true } );            // Glasses from https://sketchfab.com/3d-models/heart-glasses-ef812c7e7dc14f6b8783ccb516b3495c            glasses = await loadModel( "web/3d/heart_glasses.gltf" );            scene.add( glasses );            setText( "Loaded!" );            trackFace();        })();        </script>    </body></html>

Что дальше? Что если также добавить обнаружение эмоций на лице?

Поверите ли, что всё это возможно на одной веб-странице? Добавив 3D-объекты к функции отслеживания лиц в реальном времени, мы сотворили волшебство с помощью камеры прямо в веб-браузере. Вы можете подумать: Но очки в форме сердца существуют в реальной жизни И это правда! А что, если мы создадим что-то действительно волшебное, например шляпу которая знает, что мы чувствуем?

Давайте в следующей статье создадим волшебную шляпу (как в Хогвартсе!) для обнаружения эмоций и посмотрим, сможем ли мы сделать невозможное возможным, ещё больше используя библиотеку TensorFlow.js! До встречи завтра, в это же время.

Узнайте подробности, как получить Level Up по навыкам и зарплате или востребованную профессию с нуля, пройдя онлайн-курсы SkillFactory со скидкой 40% и промокодомHABR, который даст еще +10% скидки на обучение.

Другие профессии и курсы
Подробнее..

Перевод Отслеживание лиц в реальном времени в браузере с использованием TensorFlow.js. Часть 5

06.03.2021 20:20:06 | Автор: admin

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

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


Вы можете загрузить демоверсию этого проекта. Для обеспечения необходимой производительности может потребоваться включить в веб-браузере поддержку интерфейса WebGL. Вы также можете загрузить код и файлы для этой серии. Предполагается, что вы знакомы с JavaScript и HTML и имеете хотя бы базовое представление о нейронных сетях.

Создание волшебной шляпы

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

Чтобы создать нашу виртуальную шляпу, мы собираемся добавить графические ресурсы на веб-страницу как скрытые элементы img:

<img id="hat-angry" src="web/hats/angry.png" style="visibility: hidden;" /><img id="hat-disgust" src="web/hats/disgust.png" style="visibility: hidden;" /><img id="hat-fear" src="web/hats/fear.png" style="visibility: hidden;" /><img id="hat-happy" src="web/hats/happy.png" style="visibility: hidden;" /><img id="hat-neutral" src="web/hats/neutral.png" style="visibility: hidden;" /><img id="hat-sad" src="web/hats/sad.png" style="visibility: hidden;" /><img id="hat-surprise" src="web/hats/surprise.png" style="visibility: hidden;" />

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

let currentEmotion = "neutral";let hat = { scale: { x: 0, y: 0 }, position: { x: 0, y: 0 } };

Рисовать шляпу этого размера и в этом положении мы будем с помощью 2D-преобразования полотна в каждом кадре.

async function trackFace() {    ...    output.drawImage(        video,        0, 0, video.width, video.height,        0, 0, video.width, video.height    );    let hatImage = document.getElementById( `hat-${currentEmotion}` );    output.save();    output.translate( -hatImage.width / 2, -hatImage.height / 2 );    output.translate( hat.position.x, hat.position.y );    output.drawImage(        hatImage,        0, 0, hatImage.width, hatImage.height,        0, 0, hatImage.width * hat.scale, hatImage.height * hat.scale    );    output.restore();    ...}

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

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

const eyeDist = Math.sqrt(    ( face.annotations.leftEyeUpper1[ 3 ][ 0 ] - face.annotations.rightEyeUpper1[ 3 ][ 0 ] ) ** 2 +    ( face.annotations.leftEyeUpper1[ 3 ][ 1 ] - face.annotations.rightEyeUpper1[ 3 ][ 1 ] ) ** 2 +    ( face.annotations.leftEyeUpper1[ 3 ][ 2 ] - face.annotations.rightEyeUpper1[ 3 ][ 2 ] ) ** 2);const faceScale = eyeDist / 80;let upX = face.annotations.midwayBetweenEyes[ 0 ][ 0 ] - face.annotations.noseBottom[ 0 ][ 0 ];let upY = face.annotations.midwayBetweenEyes[ 0 ][ 1 ] - face.annotations.noseBottom[ 0 ][ 1 ];const length = Math.sqrt( upX ** 2 + upY ** 2 );upX /= length;upY /= length;hat = {    scale: faceScale,    position: {        x: face.annotations.midwayBetweenEyes[ 0 ][ 0 ] + upX * 100 * faceScale,        y: face.annotations.midwayBetweenEyes[ 0 ][ 1 ] + upY * 100 * faceScale,    }};

После сохранения названия спрогнозированной эмоции в currentEmotion отображается соответствующее изображение шляпы, и мы готовы её примерить!

if( points ) {    let emotion = await predictEmotion( points );    setText( `Detected: ${emotion}` );    currentEmotion = emotion;}else {    setText( "No Face" );}
Вот полный код этого проекта
<html>    <head>        <title>Building a Magical Emotion Detection Hat</title>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.4.0/dist/tf.min.js"></script>        <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/@tensorflow-models/face-landmarks-detection@0.0.1/dist/face-landmarks-detection.js"></script>    </head>    <body>        <canvas id="output"></canvas>        <video id="webcam" playsinline style="            visibility: hidden;            width: auto;            height: auto;            ">        </video>        <h1 id="status">Loading...</h1>        <img id="hat-angry" src="web/hats/angry.png" style="visibility: hidden;" />        <img id="hat-disgust" src="web/hats/disgust.png" style="visibility: hidden;" />        <img id="hat-fear" src="web/hats/fear.png" style="visibility: hidden;" />        <img id="hat-happy" src="web/hats/happy.png" style="visibility: hidden;" />        <img id="hat-neutral" src="web/hats/neutral.png" style="visibility: hidden;" />        <img id="hat-sad" src="web/hats/sad.png" style="visibility: hidden;" />        <img id="hat-surprise" src="web/hats/surprise.png" style="visibility: hidden;" />        <script>        function setText( text ) {            document.getElementById( "status" ).innerText = text;        }        function drawLine( ctx, x1, y1, x2, y2 ) {            ctx.beginPath();            ctx.moveTo( x1, y1 );            ctx.lineTo( x2, y2 );            ctx.stroke();        }        async function setupWebcam() {            return new Promise( ( resolve, reject ) => {                const webcamElement = document.getElementById( "webcam" );                const navigatorAny = navigator;                navigator.getUserMedia = navigator.getUserMedia ||                navigatorAny.webkitGetUserMedia || navigatorAny.mozGetUserMedia ||                navigatorAny.msGetUserMedia;                if( navigator.getUserMedia ) {                    navigator.getUserMedia( { video: true },                        stream => {                            webcamElement.srcObject = stream;                            webcamElement.addEventListener( "loadeddata", resolve, false );                        },                    error => reject());                }                else {                    reject();                }            });        }        const emotions = [ "angry", "disgust", "fear", "happy", "neutral", "sad", "surprise" ];        let emotionModel = null;        let output = null;        let model = null;        let currentEmotion = "neutral";        let hat = { scale: { x: 0, y: 0 }, position: { x: 0, y: 0 } };        async function predictEmotion( points ) {            let result = tf.tidy( () => {                const xs = tf.stack( [ tf.tensor1d( points ) ] );                return emotionModel.predict( xs );            });            let prediction = await result.data();            result.dispose();            // Get the index of the maximum value            let id = prediction.indexOf( Math.max( ...prediction ) );            return emotions[ id ];        }        async function trackFace() {            const video = document.querySelector( "video" );            const faces = await model.estimateFaces( {                input: video,                returnTensors: false,                flipHorizontal: false,            });            output.drawImage(                video,                0, 0, video.width, video.height,                0, 0, video.width, video.height            );            let hatImage = document.getElementById( `hat-${currentEmotion}` );            output.save();            output.translate( -hatImage.width / 2, -hatImage.height / 2 );            output.translate( hat.position.x, hat.position.y );            output.drawImage(                hatImage,                0, 0, hatImage.width, hatImage.height,                0, 0, hatImage.width * hat.scale, hatImage.height * hat.scale            );            output.restore();            let points = null;            faces.forEach( face => {                const x1 = face.boundingBox.topLeft[ 0 ];                const y1 = face.boundingBox.topLeft[ 1 ];                const x2 = face.boundingBox.bottomRight[ 0 ];                const y2 = face.boundingBox.bottomRight[ 1 ];                const bWidth = x2 - x1;                const bHeight = y2 - y1;                // Add just the nose, cheeks, eyes, eyebrows & mouth                const features = [                    "noseTip",                    "leftCheek",                    "rightCheek",                    "leftEyeLower1", "leftEyeUpper1",                    "rightEyeLower1", "rightEyeUpper1",                    "leftEyebrowLower", //"leftEyebrowUpper",                    "rightEyebrowLower", //"rightEyebrowUpper",                    "lipsLowerInner", //"lipsLowerOuter",                    "lipsUpperInner", //"lipsUpperOuter",                ];                points = [];                features.forEach( feature => {                    face.annotations[ feature ].forEach( x => {                        points.push( ( x[ 0 ] - x1 ) / bWidth );                        points.push( ( x[ 1 ] - y1 ) / bHeight );                    });                });                const eyeDist = Math.sqrt(                    ( face.annotations.leftEyeUpper1[ 3 ][ 0 ] - face.annotations.rightEyeUpper1[ 3 ][ 0 ] ) ** 2 +                    ( face.annotations.leftEyeUpper1[ 3 ][ 1 ] - face.annotations.rightEyeUpper1[ 3 ][ 1 ] ) ** 2 +                    ( face.annotations.leftEyeUpper1[ 3 ][ 2 ] - face.annotations.rightEyeUpper1[ 3 ][ 2 ] ) ** 2                );                const faceScale = eyeDist / 80;                let upX = face.annotations.midwayBetweenEyes[ 0 ][ 0 ] - face.annotations.noseBottom[ 0 ][ 0 ];                let upY = face.annotations.midwayBetweenEyes[ 0 ][ 1 ] - face.annotations.noseBottom[ 0 ][ 1 ];                const length = Math.sqrt( upX ** 2 + upY ** 2 );                upX /= length;                upY /= length;                hat = {                    scale: faceScale,                    position: {                        x: face.annotations.midwayBetweenEyes[ 0 ][ 0 ] + upX * 100 * faceScale,                        y: face.annotations.midwayBetweenEyes[ 0 ][ 1 ] + upY * 100 * faceScale,                    }                };            });            if( points ) {                let emotion = await predictEmotion( points );                setText( `Detected: ${emotion}` );                currentEmotion = emotion;            }            else {                setText( "No Face" );            }                        requestAnimationFrame( trackFace );        }        (async () => {            await setupWebcam();            const video = document.getElementById( "webcam" );            video.play();            let videoWidth = video.videoWidth;            let videoHeight = video.videoHeight;            video.width = videoWidth;            video.height = videoHeight;            let canvas = document.getElementById( "output" );            canvas.width = video.width;            canvas.height = video.height;            output = canvas.getContext( "2d" );            output.translate( canvas.width, 0 );            output.scale( -1, 1 ); // Mirror cam            output.fillStyle = "#fdffb6";            output.strokeStyle = "#fdffb6";            output.lineWidth = 2;            // Load Face Landmarks Detection            model = await faceLandmarksDetection.load(                faceLandmarksDetection.SupportedPackages.mediapipeFacemesh            );            // Load Emotion Detection            emotionModel = await tf.loadLayersModel( 'web/model/facemo.json' );            setText( "Loaded!" );            trackFace();        })();        </script>    </body></html>

Что дальше? Возможен ли контроль по состоянию глаз и рта?

В этом проекте собраны воедино все куски, созданные ранее в этой серии статей в целях развлечения с визуальными образами. А что, если бы можно было реализовать в нём взаимодействие с лицом?

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

Узнайте подробности, как получить Level Up по навыкам и зарплате или востребованную профессию с нуля, пройдя онлайн-курсы SkillFactory со скидкой 40% и промокодомHABR, который даст еще +10% скидки на обучение.

Другие профессии и курсы
Подробнее..

Текущее положение дел по распознаванию лиц и камерам наблюдений в Москве и мире

20.04.2021 22:19:53 | Автор: admin
image

Количество камер наблюдения в столицах и ключевых городах мира. Слева направо, сверху вниз: Москва 193 000, Лондон 627 707, Нью-Йорк 31 490, Пекин 1 150 000, Париж 26 834, Шэньчжэнь 400 000.

Как развивались события и росла (нейро)сеть камер наблюдения в Москве.

2016 год


Февраль 2016 года. NtechLab выпустила в открытый доступ FindFace: пользователи могли находить людей во ВКонтакте по фотографии на улице или в транспорте.

20 апреля 2016 года пользователи Двача деанонимизировали российских порноактрис с помощью FindFace.

7 июля 2016 года Даниил Туровский рассказал о приложении FindFace и технологиях тотальной слежки

2017


28 сентября 2017 года власти Москвы начали подключать к городской системе видеонаблюдения технологию распознавания лиц российской компании NTechLab. За два месяца работы системы в тестовом режиме с её помощью удалось задержать шестерых преступников, которые находились в федеральном розыске.

2018 год


FindFace Security от NtechLab позволила задержать более 180 правонарушителей на ЧМ-2018, часть задержанных находилась в федеральном розыске.

2019 год


27 июня 2019 года. МВД раскрыло результаты тестового внедрения систем распознавания лиц в метро и на улицах Москвы. С помощью 1000 камер снаружи подъездов жилых домов сотрудники правоохранительных органов задержали 90 человек.

В ноябре 2019 года мэрия заказала 258 серверов, 105 компьютеров и систему хранения данных общей емкостью 9 петабайт на 1,2 млрд руб.

2020 год


В январе 2020 года был объявлен конкурс на 1,9 млрд руб. на поставку оборудования для системы распознавания лиц на транспорте.

С 1 сентября 2020 года в московском метрополитене заработала система видеонаблюдения. Она охватывает все станции метро. Теперь планируется развивать эту систему в направлении пассажирских сервисов. Одним из них станет FacePay, который позволяет оплачивать проезд по скану лица.

15 ноября 2020 года Департамент правительства Москвы по конкурентной политике выделил 252,6 млн руб. для расширения системы хранения данных интеллектуальной системы видеонаблюдения (ИСВН) в метро. Соответствующий тендер опубликован на портале госзакупок. Подрядчик должен будет установить не менее 2220 жестких дисков объемом не менее 4 Тбайт. Общая вместимость хранилища записей с камер наблюдения увеличится минимум на 8880 Тбайт. (Источник)

Национальный институт стандартов и технологий США (NIST) провёл исследование и подтвердил, что системы не справляются с масками. Но уже спустя полгода компании решили эту проблему в исследовании NIST от декабря 2020 года все три компании из России попали в список тех, кто совершает меньше всего ошибок.

2021


2 марта 2021 года. Более 900 преступников задержали благодаря системе распознавания лиц в Московском метрополитене за полгода.

Мэрия Москвы до 2025 года потратит 2,917 млрд руб. на систему видеонаблюдения с функцией распознавания лиц в Новой Москве.

Затраты Москвы на модернизацию систем умного видеонаблюдения и анализа информации о гражданах постоянно растут. Если в 2019 году ДИТ потратил на эти цели 60,8 млрд руб., в 2020-м около 68 млрд руб., то по итогам 2021 года расходы ожидаются на уровне 70,8 млрд руб., следует из информации на сайте Мосгордумы.

image

Центр видеонаблюдения, куда стекаются данные ДИТ. Фото Антона Тушина для ТАСС

Мировая статистика по камерам слежения


Отчет Surfshark.

  • Ченнаи в Индии город с самой высокой плотностью камер видеонаблюдения: 657,28 на км2.
  • В Лондоне, Англия, больше всего камер видеонаблюдения на кв. км за пределами Азии: 399,27/ км2.
  • Нью-Йорк город США с самой высокой плотностью камер видеонаблюдения: 25,97/км2.
  • В Пекине в Китае больше всего камер (1,15 млн), но он занимает 10-е место по плотности (277,51/км2).
  • Шесть из 10 городов с наибольшей плотностью камер видеонаблюдения находятся в Китае, три Индии.


image


Топ-9 городов по количеству установленных камер. С 4 по 22 места Индия и Китай. Москва на 23 месте.

image


Топ-9 городов по концентрации на кв км. Париж 12 место, Багдад 16 место, Гвадалахара 17 место, Сингапур 23 место, Буэнос-Айрес 29 место, Москва 30 место, Санкт-Петербург 46 место

Не только камеры, не только лицо


image

2 февраля 2021 года. Министерство внутренних дел России намерено при помощи искусственного интеллекта определять преступников по голосу. Тендер на соответствующую программу и оборудование был размещен на официальном сайте.

А разработки MIT позволяют видеть сквозь стены:

image

Статья на Хабре: Машинное (радио)зрение видит сквозь стены



Выводы


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

Понятие приватности нужно оставить в XX веке. В XXI оно не применимо. Все будут знать про вас всё, это лишь вопрос времени. И вы от этого только выиграете.
Александр Минин, гендиректор NtechLab


Источники


Подробнее..

Как мы построили Computer Vision из подручных материалов, чтобы сделать гифки

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

Меня зовут Денис Власов, я Data Scientist в Учи.ру. С помощью моделей машинного обучения из записей онлайн-уроков мы сделали гифки последовательность из нескольких кадров с наиболее яркими эмоциями учеников. Эти гифки получили их родители в e-mail-рассылке. Вместе с Data Scientist @DariaV Дашей Васюковой расскажем, как без экспертизы в Computer Vision, а только с помощью открытых библиотек и готовых моделей сделать MVP, в основе которого лежат low-res видео. В конце бонус виджет для быстрой разметки кадров.

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

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

Маркеры начала и конца урока

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

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

Разбили видео на кадры

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

Научились детектировать детские улыбки (и не только)

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

Проблема 1. Распознавать лица на картинках низкого качества сложнее

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

Стандартный детектор DNN Face Detector из библиотеки OpenCV, который мы сначала взяли за основу, на наших данных давал неточные результаты. Оказалось, что алгоритм недостаточно хорошо справляется с реальными кадрами из видеочатов: иногда пропускает лица, которые явно есть в кадре, из двух лиц находил только одно или определял лица там, где их нет.

Стандартный детектор DNN Face Detector мог определить как лицо узор на занавеске, игрушечного медведя или даже композицию из картин на стене и стулаСтандартный детектор DNN Face Detector мог определить как лицо узор на занавеске, игрушечного медведя или даже композицию из картин на стене и стула

Поэтому мы решили попробовать обучить свой детектор. Для этого взяли реализацию RetinaNet-модели на PyTorch. В качестве данных для обучения подали результаты работы стандартного детектора и убедились, что новая модель учится находить лица. Затем подготовили обучающую и валидационную выборку, просматривая и при необходимости исправляя результаты работы детектора на новых кадрах: исправлять разметку работающей модели получается быстрее, чем отмечать лица на кадре с нуля.

Размечали итеративно: после добавления новой порции размеченных кадров мы заново обучали модель. А после проверки ее работы сохраняли разметку для новых кадров, наращивая обучающую выборку. Всего мы разметили 2624 кадра из 388 видеозаписей, на которых в сумме было 3325 лиц.

Таким образом удалось обучить более чувствительный в наших условиях детектор. В валидационной выборке из 140 кадров старый детектор нашел 150 лиц, а пропустил 38. Новый же пропустил только 5, а 183 обнаружил верно.

Проблема 2. В кадре присутствует не только ребенок

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

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

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

  • возраст людей на кадре с низким разрешением становится неочевидным;

  • дети присутствуют в кадре практически весь урок, а взрослые минуты.

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

На всех трех кадрах родитель присутствует, но по отдельному кадру найти его бывает непростоНа всех трех кадрах родитель присутствует, но по отдельному кадру найти его бывает непросто

Вторая модель должна была находить именно такие родительские плечи. Очевидно, что в этой задаче детектор лиц не применим, поэтому надо обучаться на кадрах целиком. Конечно, таких датасетов мы не нашли в публичном доступе и разметили около 250 000 кадров, на которых есть часть родителя, и кадры без них. Разметки на порядок больше, чем в других задачах, потому что размечать гораздо легче: можно смотреть не отдельные кадры, а отрезки видео и в несколько кликов отмечать, например, что вот эти 15 минут (900 кадров!) родитель присутствовал.

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

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

Проблема 3. Дети улыбаются по-разному

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

За основу классификатора настроения мы взяли предобученную модель ResNet34 из библиотеки fast.ai. Эту же библиотеку использовали для дообучения модели в два этапа: сначала на публичных датасетах facial_expressions и SMILEsmileD с веселыми и нейтральными лицами, потом на нашем размеченном вручную датасете с кадрами с камер учеников. Публичные датасеты решили включить, чтобы расширить размер выборки и помочь модели более качественными изображениями, чем кадры видео с планшетов и веб-камер наших учеников.

Размечали с помощью кастомного виджета. Все изображения подвергались одной и той же процедуре предобработки:

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

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

  3. Аугментация. Позволяет в разы увеличить эффективный размер выборки и учесть особенности данных.

  4. Нормализация цветов с помощью CLAHE normalizer из библиотеки OpenCV. По ощущениям, такая нормализация лучше других вытягивает контраст на пересвеченных или темных изображениях.

Дообучаем модель для распознавания улыбок

1. Аугментации

При дообучении мы использовали достаточно жесткие аугментации:

  • Отражали изображение по горизонтали.

  • Поворачивали на случайную величину.

  • Применяли три разных искажения для изменения контраста и яркости.

  • Брали не всю картинку, а квадрат, составляющий не менее 60% от площади исходного изображения.

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

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

Пример аугментаций на одном изображении. Для наглядности аугментации сделаны до масштабирования к разрешению 64х64Пример аугментаций на одном изображении. Для наглядности аугментации сделаны до масштабирования к разрешению 64х64Код для аугментаций
# ! pip freeze | grep fastai# fastai==1.0.44import fastaiimport matplotlib.pyplot as pltfrom matplotlib import cmfrom matplotlib import colorsimport seaborn as sns%matplotlib inlinefrom pylab import rcParamsplt.style.use('seaborn-talk')rcParams['figure.figsize'] = 12, 6path = 'facial_expressions/images/'def _side_cutoff(    x,    cutoff_prob=0.25,    cutoff_intensity=(0.1, 0.25)):    if np.random.uniform() > cutoff_prob:        return x    # height and width    h, w = x.shape[1:]    h_cutoff = np.random.randint(        int(cutoff_intensity[0]*h), int(cutoff_intensity[1]*h)    )    w_cutoff = np.random.randint(        int(cutoff_intensity[0]*w), int(cutoff_intensity[1]*w)    )        cutoff_side = np.random.choice(        range(4),        p=[.34, .34, .16, .16]    ) # top, bottom, left, right.    if cutoff_side == 0:        x[:, :h_cutoff, :] = 0    elif cutoff_side == 1:        x[:, h-h_cutoff:, :] = 0    elif cutoff_side == 2:        x[:, :, :w_cutoff] = 0    elif cutoff_side == 3:        x[:, :, w-w_cutoff:] = 0    return x# side cutoff goes frist.side_cutoff = fastai.vision.TfmPixel(_side_cutoff, order=99)augmentations = fastai.vision.get_transforms(    do_flip=True,    flip_vert=False,    max_rotate=25.0,    max_zoom=1.25,    max_lighting=0.5,    max_warp=0.0,    p_affine=0.5,    p_lighting=0.5,        xtra_tfms = [side_cutoff()])def get_example():    return fastai.vision.open_image(        path+'George_W_Bush_0016.jpg',    )def plots_f(rows, cols, width, height, **kwargs):    [        get_example()        .apply_tfms(            augmentations[0], **kwargs        ).show(ax=ax)        for i,ax in enumerate(            plt.subplots(                rows,                cols,                figsize=(width,height)            )[1].flatten())    ]plots_f(3, 5, 15, 9, size=size)

2. Нормализация цвета

Мы попробовали несколько вариантов предобработки и остановились на CLAHE нормализации. Этим способом яркость и гамма выравниваются не по всему изображению, а по его частям. Результат будет приемлемый, даже если на одном изображении есть и затемненные участки, и засвеченные.

Пример нормализации цвета на изображениях из публичного датасетаПример нормализации цвета на изображениях из публичного датасетаКод для нормализации цвета
# pip freeze | grep opencv# > opencv-python==4.5.2.52import cv2import matplotlib.pyplot as pltfrom matplotlib import cmfrom matplotlib import colorsimport seaborn as sns%matplotlib inlinefrom pylab import rcParamsplt.style.use('seaborn-talk')rcParams['figure.figsize'] = 12, 6path = 'facial_expressions/images/'imgs = [    'Guillermo_Coria_0021.jpg',    'Roger_Federer_0012.jpg',]imgs = list(    map(        lambda x: path+x, imgs    ))clahe = cv2.createCLAHE(    clipLimit=2.0,    tileGridSize=(4, 4))rows_cnt = len(imgs)cols_cnt = 4imsize = 3fig, ax = plt.subplots(    rows_cnt, cols_cnt,    figsize=(cols_cnt*imsize, rows_cnt*imsize))for row_num, f in enumerate(imgs):    img = cv2.imread(f)    col_num = 0        img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)    ax[row_num, col_num].imshow(img, cmap='gray')    ax[row_num, col_num].set_title('bw', fontsize=14)    col_num += 1    img_normed = cv2.normalize(        img,        None,        alpha=0,        beta=1,        norm_type=cv2.NORM_MINMAX,        dtype=cv2.CV_32F    )    ax[row_num, col_num].imshow(img_normed, cmap='gray')    ax[row_num, col_num].set_title('bw normalize', fontsize=14)    col_num += 1        img_hist_normed = cv2.equalizeHist(img)    ax[row_num, col_num].imshow(img_hist_normed, cmap='gray')    ax[row_num, col_num].set_title('bw equalizeHist', fontsize=14)    col_num += 1        img_clahe = clahe.apply(img)    ax[row_num, col_num].imshow(img_clahe, cmap='gray')    ax[row_num, col_num].set_title('bw clahe_norm', fontsize=14)    col_num += 1        for col in ax[row_num]:        col.set_xticks([])        col.set_yticks([])plt.show()

В итоге мы получили модель, способную отличить улыбку от нейтрального выражения лица в с качеством 0.93 по метрике ROC AUC. Иными словами, если взять из выборки по случайному кадру с улыбкой и без, то с вероятностью 93% модель присвоит большую вероятность улыбки кадру с улыбающимся лицом. Этот показатель мы использовали для сравнения разных вариантов дообучения и пайплайнов. Но интуитивно кажется, что это достаточно высокий уровень точности: даже человек не всегда может определить эмоцию на лице другого человека. К тому же в реальности существует гораздо больше выражений лиц кроме однозначной радости и однозначной печали.

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

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

3. Увеличение объема выборки

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

Мы несколько раз дообучили модель на публичных датасетах и подвыборках разного размера из трейна и проверили качество на валидационной выборке. На графике видно, что качество растет по мере увеличения объема подвыборки, поэтому мы разметили дополнительно еще 3 тыс. кадров: финальную модель обучали на выборке из 8500 кадров. Скорее всего и при таком объеме данных качество еще не начало выходить на плато, но перепроверять это мы не стали.

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

Качество на отложенной выборке растет по мере увеличения выборки для дообученияКачество на отложенной выборке растет по мере увеличения выборки для дообучения

4. Картинки Google для обогащения выборки

Мы пробовали спарсить первые 1000 результатов картинок по запросам в духе happy, unhappy, smiling, neutral и т. д. Не ожидали получить данные высокого качества, поэтому планировали потом просмотреть их глазами и удалить совсем неподходящие. В итоге мы быстро поняли, что никакая фильтрация эти картинки не спасет, поэтому отказались от этой идеи совсем.

Примеры изображений по запросам happy и unhappyПримеры изображений по запросам happy и unhappy

В итоге мы получили четыре модели, которые с высокой точностью могли показать:

  • есть ли в кадре лицо;

  • с какой вероятностью этот человек улыбается;

  • ребенок это или взрослый;

  • есть ли в кадре взрослый, даже если мы не нашли лица.

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

Собрали гифку

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

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

Примеры итоговых GIF с улыбками нашей коллеги и ее детейПримеры итоговых GIF с улыбками нашей коллеги и ее детей

Что мы в итоге получили?

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

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

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

Статистика дисконнектов. В этом уроке был единственный дисконнект на стороне ученикаСтатистика дисконнектов. В этом уроке был единственный дисконнект на стороне ученика

Другой пример трекинг настроения ученика на протяжении урока. Он позволяет проанализировать ход занятия и понять, нужно ли что-то менять в его структуре.

Виджеты

Все данные мы размечали сами и делали это довольно быстро (примерно 100 кадров в минуту). В этом нам помогали самописные виджеты:

  1. Виджет для разметки кадров с улыбками.

  2. Виджет для разметки кадров с детьми и взрослыми.

  3. Виджет для разметки кадров с плечом или локтем родителя.

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

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

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

Видео работы виджета

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

Код виджета
import pandas as pdimport numpy as npimport datetimeimport randomimport osimport ipywidgets as widgetsfrom IPython.display import displayfrom pathlib import Pathclass BulkLabeler():    def __init__(self, frames_path, annotations_path,                 labels = ['0', '1'],                 predict_fn = None,                 frame_width=120,                 num_frames = 27,                 face_width = 120,                 num_faces = 27,                 myname = '?',                 ):        self.predict_fn = predict_fn        self.labels = labels        self.frames_path = frames_path        self.frame_width = frame_width        self.num_frames = num_frames        self.face_width = face_width        self.num_faces = num_faces        self.myname = myname        self.faces_batch = []                # get annotations        self.annotations_path = annotations_path        processed_videos = []        if annotations_path.exists():            annotations = pd.read_csv(annotations_path)            processed_videos = annotations.file.str.split('/').str[-3].unique()        else:            with open(self.annotations_path, 'w') as f:                f.write('file,label,by,created_at\n')                # get list of videos        self.video_ids = [x for x in os.listdir(frames_path)                           if x not in processed_videos]        random.shuffle(self.video_ids)        self.video_ind = -1                self._make_video_widgets_row()        self._make_frames_row()        self._make_range_slider()        self._make_buttons_row()        self._make_faces_row()        self._make_video_stats_row()                display(widgets.VBox([self.w_video_row,                              self.w_frames_row,                              self.w_slider_row,                              self.w_buttons_row,                              self.w_faces_row,                              self.w_faces_label,                              self.w_video_stats]))        self._on_next_video_click(0)            ### Video name and next video button        def _make_video_widgets_row(self):        # widgets for current video name and "Next video" button        self.w_current_video = widgets.Text(            value='',            description='Current video:',            disabled=False,            layout = widgets.Layout(width='500px')            )                self.w_next_video_button = widgets.Button(            description='Next video',            button_style='info', # 'success', 'info', 'warning', 'danger' or ''            tooltip='Go to the next video',            icon='right-arrow'        )                self.w_video_row = widgets.HBox([self.w_current_video, self.w_next_video_button])                self.w_current_video.observe(self._on_video_change, names='value')        self.w_next_video_button.on_click(self._on_next_video_click)                        def _on_next_video_click(self, _):        while True:            self.video_ind += 1            current_video = self.video_ids[self.video_ind]            if next(os.scandir(self.frames_path/current_video/'student_faces'), None) is not None:                break        self.w_current_video.value = current_video                    def _on_video_change(self, change):        self.video_id = change['new']        self.frame_nums_all = sorted(int(f.replace('.jpg',''))                                      for f in os.listdir(self.frames_path/self.video_id/'student_src'))        start, stop = min(self.frame_nums_all), max(self.frame_nums_all)        self.w_range_slider.min = start        self.w_range_slider.max = stop        step = self.frame_nums_all[1] - self.frame_nums_all[0] if len(self.frame_nums_all)>1 else 1        self.w_range_start.step = step        self.w_range_stop.step = step        # change to slider value will cause frames to be redrawn        self.w_range_slider.value = [start, stop]               # reset faces        self.faces_df = None        self._reset_faces_row()        self.w_video_stats.value = f'Video {self.video_id}  no annotations yet.'                def _close_video_widgets_row(self):        self.w_current_video.close()        self.w_next_video_button.close()        self.w_video_row.close()        ### Video frames box        def _make_frames_row(self):        frame_boxes = []        self.w_back_buttons = {}        self.w_forward_buttons = {}        for i in range(self.num_frames):            back_button = widgets.Button(description='<',layout=widgets.Layout(width='20px',height='20px'))            self.w_back_buttons[back_button] = i            back_button.on_click(self._on_frames_back_click)            label = widgets.Label(str(i+1), layout = widgets.Layout(width=f'{self.frame_width-50}px'))            forward_button = widgets.Button(description='>',layout=widgets.Layout(width='20px',height='20px'))            self.w_forward_buttons[forward_button] = i            forward_button.on_click(self._on_frames_forward_click)            image = widgets.Image(width=f'{self.frame_width}px')            frame_boxes.append(widgets.VBox([widgets.HBox([back_button, label, forward_button]),                                              image]))                    self.w_frames_row = widgets.GridBox(frame_boxes,                                             layout = widgets.Layout(width='100%',                                                                     display='flex',                                                                     flex_flow='row wrap'))            def _on_frames_back_click(self, button):        frame_ind = self.w_back_buttons[button]        frame = int(self.w_frames_row.children[frame_ind].children[0].children[1].value)        start, stop = self.w_range_slider.value        self.w_range_slider.value = [frame, stop]            def _on_frames_forward_click(self, button):        frame_ind = self.w_forward_buttons[button]        frame = int(self.w_frames_row.children[frame_ind].children[0].children[1].value)        start, stop = self.w_range_slider.value        self.w_range_slider.value = [start, frame]            def _close_frames_row(self):        for box in self.w_frames_row.children:            label_row, image = box.children            back, label, forward = label_row.children            image.close()            back.close()            label.close()            forward.close()            box.close()        self.w_frames_row.close()                    ### Frames range slider                        def _make_range_slider(self):        self.w_range_start = widgets.BoundedIntText(                                        value=0,                                        min=0,                                        max=30000,                                        step=1,                                        description='Frames from:',                                        disabled=False,                                        layout = widgets.Layout(width='240px')                                    )        self.w_range_stop = widgets.BoundedIntText(                                        value=30000,                                        min=0,                                        max=30000,                                        step=1,                                        description='to:',                                        disabled=False,                                        layout = widgets.Layout(width='240px')                                    )        self.w_range_slider = widgets.IntRangeSlider(            value=[0, 30000],            min=0,            max=30000,            step=1,            description='',            disabled=False,            continuous_update=False,            orientation='horizontal',            readout=True,            readout_format='d',            layout=widgets.Layout(width='500px')        )        self.w_range_flip = widgets.Button(description='Flip range',            button_style='', # 'success', 'info', 'warning', 'danger' or ''            tooltip='Invert frames selection',            layout = widgets.Layout(width=f'{self.frame_width}px'),            icon='retweet'                                          )                self.w_range_slider.observe(self._on_slider_change, names='value')        self.w_range_start.observe(self._on_range_start_change, names='value')        self.w_range_stop.observe(self._on_range_stop_change, names='value')        self.w_range_flip.on_click(self._on_range_flip)        self.w_slider_row = widgets.HBox([self.w_range_start,                                          self.w_range_slider,                                          self.w_range_stop,                                          self.w_range_flip])        def _close_range_slider(self):        self.w_range_start.close()        self.w_range_stop.close()        self.w_range_slider.close()        self.w_range_flip.close()        self.w_slider_row.close()            def _on_range_flip(self, _):        start, stop = self.w_range_slider.value        left, right = self.w_range_slider.min, self.w_range_slider.max        if start==left and right==stop:            pass        elif start - left > right - stop:            self.w_range_slider.value=[left, start]        else:            self.w_range_slider.value=[stop, right]                                                   def _on_range_start_change(self, change):        new_start = change['new']        start, stop = self.w_range_slider.value        self.w_range_slider.value = [new_start, stop]                    def _on_range_stop_change(self, change):        new_stop = change['new']        start, stop = self.w_range_slider.value        self.w_range_slider.value = [start, new_stop]                    def _on_slider_change(self, change):        start, stop = change['new']        # update text controls        self.w_range_start.max = stop        self.w_range_start.value = start        self.w_range_stop.min = start        self.w_range_stop.max = self.w_range_slider.max        self.w_range_stop.value = stop        # show frames that fit current selection        frame_nums = [i for i in self.frame_nums_all if i>=start and i<=stop]        N = len(frame_nums)        n = self.num_frames        inds = [int(((N-1)/(n-1))*i) for i in range(n)]        # load new images into image widgets        for ind, box in zip(inds, self.w_frames_row.children):            frame_num = frame_nums[ind]            filename = self.frames_path/self.video_id/'student_src'/f'{frame_num}.jpg'            with open(filename, "rb") as image:                f = image.read()            label, image = box.children            label.children[1].value = str(frame_num)            image.value = f            ### Buttons row        def _make_buttons_row(self):        labels = list(self.labels)        if self.predict_fn is not None:            labels.append('model')        self.w_default_label = widgets.ToggleButtons(options=labels,                                                      value=self.labels[0],                                                      description='Default label:')                self.w_next_batch_button = widgets.Button(description='New batch',            button_style='info', # 'success', 'info', 'warning', 'danger' or ''            tooltip='Show next batch of faces from current frame range',            icon='arrow-right'        )        self.w_save_button = widgets.Button(description='Save labels',            button_style='success', # 'success', 'info', 'warning', 'danger' or ''            tooltip='Save current labels',            icon='check'        )        self.w_buttons_row = widgets.HBox([self.w_default_label, self.w_next_batch_button, self.w_save_button])        self.w_next_batch_button.on_click(self._on_next_batch_click)        self.w_save_button.on_click(self._on_save_labels_click)                def _close_buttons_row(self):        self.w_default_label.close()        self.w_next_batch_button.close()        self.w_save_button.close()        self.w_buttons_row.close()                def _on_next_batch_click(self, _):        if self.faces_df is None:             self._create_faces_df()        # select a sample from faces_df        start, stop = self.w_range_slider.value        subdf = self.faces_df.loc[lambda df: df.frame_num.ge(start)&                                             df.frame_num.le(stop)&                                             df.label.eq('')]        num_faces = min(len(subdf), self.num_faces)                if num_faces == 0:            self.faces_batch = []            self.w_faces_label.value = 'No more unlabeled images in this frames range'            self.w_faces_label.layout.visibility = 'visible'            for box in self.w_faces_row.children:                box.layout.visibility = 'hidden'        else:            self.w_faces_label.layout.visibility = 'hidden'            self.faces_batch = subdf.sample(num_faces).index            # if we have a model then we use it to sort images            if self.predict_fn is not None:                probs, labels = self._predict()                # sort faces according to probability                ind = sorted(range(len(probs)), key=probs.__getitem__)                self.faces_batch = [self.faces_batch[i] for i in ind]                labels = [labels[i] for i in ind]            # create labels for each face            if self.w_default_label.value != 'model':                labels = [self.w_default_label.value]*len(self.faces_batch)            # update faces UI            for facefile, label, box in zip(self.faces_batch, labels, self.w_faces_row.children):                image, buttons = box.children                with open(self.frames_path/facefile, "rb") as im:                    image.value = im.read()                buttons.value = label                box.layout.visibility = 'visible'            if len(self.faces_batch) < len(self.w_faces_row.children):                for box in self.w_faces_row.children[len(self.faces_batch):]:                    box.layout.visibility = 'hidden'        def _predict(self):        probs = []        labels = []        for facefile in self.faces_batch:            prob, label = self.predict_fn(self.frames_path/facefile)            probs.append(prob)            labels.append(label)        self.faces_df.loc[self.faces_batch, 'prob'] = probs        return probs, labels                    def _on_save_labels_click(self, _):        self.w_save_button.description='Saving...'                        with open(self.annotations_path, 'a') as f:            for file, box in zip(self.faces_batch, self.w_faces_row.children):                label = box.children[1].value                self.faces_df.at[file,'label'] = label                print(file, label, self.myname, str(datetime.datetime.now()),sep=',', file=f)                # update current video statistics        stats = self.faces_df.loc[self.faces_df.label.ne(''),'label'].value_counts().sort_index()        stats_str = ', '.join(f'{label}: {count}' for label, count in stats.items())        self.w_video_stats.value = f'Video {self.video_id}  {stats_str}.'                self.w_save_button.description = 'Save labels'        # ask for next batch        self._on_next_batch_click(0)            ### Faces row        def _make_faces_row(self):        face_boxes = []        for i in range(self.num_faces):            image = widgets.Image(width=f'{self.face_width}px')            n = len(self.labels)            toggle_buttons_width = int(((self.face_width-5*(n-1))/n))            toggle_buttons = widgets.ToggleButtons(options=self.labels,                                                    value=self.w_default_label.value,                                                    style=widgets.ToggleButtonsStyle(button_width=f'{toggle_buttons_width}px'))            face_boxes.append(widgets.VBox([image, toggle_buttons]))                    self.w_faces_row = widgets.GridBox(face_boxes,                                            layout = widgets.Layout(width='100%',                                                                    display='flex',                                                                    flex_flow='row wrap'))        self.w_faces_label = widgets.Label()        self._reset_faces_row()            def _close_faces_row(self):        for box in self.w_faces_row.children:            image, buttons = box.children            for w in [image, buttons, box]:                w.close()        self.w_faces_row.close()        self.w_faces_label.close()            def _reset_faces_row(self):        for box in self.w_faces_row.children:            box.layout.visibility = 'hidden'        self.w_faces_label.layout.visibility = 'visible'        self.w_faces_label.value = 'Press "New batch" button to see a new batch of faces'        self.faces_batch = []            ### Video statistics row        def _make_video_stats_row(self):        self.w_video_stats = widgets.Label('No video currently selected')        def _close_video_stats_row(self):        self.w_video_stats.close()                def _create_faces_df(self):        folder = Path(self.video_id,'student_faces')        df = pd.DataFrame({'file':[folder/f for f in os.listdir(self.frames_path/folder)]})        df['frame_num'] = df.file.apply(lambda x: int(x.stem.split('_')[0]))        df['label'] = '' #TODO maybe existing annotations?        df['prob'] = np.nan        df = df.sort_values(by='frame_num').set_index('file')        self.faces_df = df                    def close(self):        self._close_video_widgets_row()        self._close_frames_row()        self._close_range_slider()        self._close_buttons_row()        self._close_faces_row()        self._close_video_stats_row()
Подробнее..

Терминалы распознавания лиц в системах контроля доступа

11.09.2020 14:12:59 | Автор: admin
Распознавание лиц в системах контроля доступа отвечает растущему спросу на бесконтактные решения в области идентификации. Сегодня данный способ биометрической идентификации является общемировым трендом: среднегодовой рост объема рынка систем на базе распознавания лиц оценивается аналитиками в 20%. Согласно прогнозам, в 2023 году эта цифра увеличится до 4 млрд USD.



Интеграция терминалов с системой контроля доступа


Распознавание лиц в качестве способа идентификации может применяться в СКУД для контроля доступа, учета рабочего времени и интеграции с CRM- и ERP-системами. Лидирующими производителями терминалов распознавания лиц на российском рынке являются Hikvision, Suprema, Dahua и ZKteco.

Интеграция терминалов распознавания лиц с системой контроля доступа может осуществляться тремя способами, различие которых заключается в интерфейсе связи и функционале SDK. Первый способ позволяет добавлять новые данные сотрудников или посетителей непосредственно в интерфейсе СКУД, без добавления в терминалы за счет SDK терминала. При втором способе добавление новых пользователей осуществляется как в интерфейсе СКУД, так и непосредственно в терминалах, что менее удобно и более трудозатратно. В обоих случаях подключение осуществляется по интерфейсу Ethernet. Третий способ подключение по интерфейсу Wiegand, но в этом случае у терминалов и СКУД будут раздельные базы данных.

В обзоре будут рассмотрены решения с подключением по интерфейсу Ethernet. Возможность добавления пользователей в интерфейсе системы определяется SDK терминала.Чем шире возможности СКУД, тем больший функционал будет возможно реализовать при помощи терминалов. Например, интеграция системы контроля доступа PERCo-Web с терминалами Suprema позволяет добавлять данные непосредственно в интерфейсе ПО системы. Среди других возможностей занесение и сохранение фотографий сотрудников и посетителей для идентификации, осуществление конфигурации устройств и управления ими в режиме онлайн.

Все события проходов через терминалы сохраняются в системе. Система позволяет назначать алгоритм реакций на события, полученных с терминалов. При проходе сотрудника с распознаванием по лицу можно сформировать уведомляющее событие, которое будет отправляться на Viber или e-mail оператора системы. Система поддерживает работу с терминалами Face Station 2 и FaceLite от Suprema, ProfaceX, FaceDepot 7А, Facedepot 7 В, SpeedFace V5L от ZKteco. При проходе сотрудника или посетителя с повышенной температурой в СКУД формируется событие, на основании которого доступ может быть автоматически заблокирован.

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

Рассмотрим с точки зрения работы в СКУД технические характеристики следующих моделей данных производителей:

Face Station 2 и FaceLite от Suprema



ProfaceX, FaceDepot 7А, Facedepot 7 В, SpeedFace V5L от ZKteco



DS-K1T606MF, DS-K1T8105E и DS-K1T331W от Hikvision



ASI7223X-A, ASI7214X от Dahua



Защита от эмуляции


Распознавание лиц может базироваться на 2D или 3D- технологиях. Первая из них является более бюджетной, что сказывается и на стоимости терминалов. Среди ее недостатков высокие требования к освещению, более низкая в сравнении с 3D статистическая достоверность, неспособность учитывать мимику лица. Увеличить точность идентификации терминалов на базе 2D позволяют инфракрасные камеры.

Терминалы, работающие по 3D- технологии, являются более дорогостоящими, но обеспечивают высокую точность и достоверность идентификации и демонстрируют способность работы в условиях низкой освещенности. В терминалах Suprema и ZKteco для защиты от предъявления фотографии применяется детектирование живого лица, основанное на инфракрасной подсветке. Терминалы Hikvision используют алгоритм глубокого машинного обучения и детекции подлинности биометрических данных лица. Терминалы распознавания лиц от Dahua используют технологии искусственного интеллекта и глубинного изучения с поддержкой детекции витальности.

Скорость идентификации


Скорость идентификации терминалов распознавания лиц имеет особенно большое значение для объектов с интенсивным потоком посетителей: офисов крупных компаний, промышленных предприятий, мест массового пребывания людей. Высокая скорость идентификации препятствует образованию очередей и обеспечивает максимальную пропускную способность. Терминалы Hikvision DS-K1T331W, Dahua ASI7223X-A и ASI7214X распознают лица всего за 0,2 секунды. У модели DS-K1T606MF идентификация осуществляется за 0,5 секунд, у DS-K1T8105E менее чем за 1 секунду. Скорость идентификации терминалов Face Station и FaceDepot 7А составляет менее 1 секунды.

Двухфакторная идентификация




Удобным решением для работы в СКУД являются терминалы распознавания лиц, поддерживающие также и другие способы идентификации: например, доступ по карте, отпечатку пальца, ладони или смартфону. Такие решения позволяют усилить контроль доступа на объект за счет двухфакторной идентификации. Терминалы FaceLite и FaceStation 2 отличает наличие встроенного считывателя бесконтактных карт доступа, в других рассматриваемых нами моделях считыватель можно подключить дополнительно. Терминалы ZKteco поддерживают также идентификацию по ладони и коду. Терминалы Hikvision DS-K1T606MF поддерживают идентификацию по отпечаткам пальцев и картам Mifare, DS-K1T8105E имеет встроенный считыватель карт EM-Marine, к терминалу DS-K1T331W может быть подключен считыватель бесконтактных карт. Терминал ASI7214X также поддерживает работу с бесконтактными картами и отпечатками пальцев.

Измерение температуры


Одним из драйверов роста рынка решений по распознаванию лиц в системах контроля доступа стала пандемия Covid19, поэтому терминалы распознавания лиц с возможностью контроля температуры тела получили широкое распространение. Данный функционал из рассматриваемых нами моделей позволяют реализовать терминалы SpeedFace V5L, которые также определяют наличие на лице маски. Измерение температуры происходит бесконтактно, что снижает риск передачи инфекций и позволяет минимизировать
необходимость антисептической обработки устройства после каждого измерения.
Удобным решением является установка параметров контроля температуры и наличия маски в интерфейсе СКУД, если SDK терминала позволяет заносить данные непосредственно в системе.

Количество шаблонов лиц


Ёмкость шаблона максимальное количество наборов данных, которые могут храниться в системе. Чем выше данный показатель, тем выше точность идентификации. Большой ёмкостью распознавания обладают терминалы Face Station 2 и FaceLite. Они обрабатывают до 900 000 шаблонов. Терминалы ProFace X хранят в памяти по 30 000 шаблонов, FaceDepot 7А и Facedepot 7 В по 10 000 шаблонов, SpeedFace V5L 6000.
Терминалы ASI7223X-A и ASI7214X вмещают по 100 000 шаблонов.

Количество пользователей и событий


Количество пользователей в памяти терминала распознавания лиц определяет количество максимально возможных идентификаторов для доступа на объект. Чем крупнее объект, тем выше должен быть данный показатель. Память контроллеров Face Station 2 и FaceLite рассчитана на 30000 пользователей, как и память ProfaceX. FaceDepot 7А, Facedepot 7 В, SpeedFace V5L обрабатывают данные 10 000 человек. Память терминала DS-K1T8105E рассчитана на 1600 пользователей, DS-K1T331 на 3000, DS-K1T606MF на 3200 пользователей. Терминалы ASI7223X-A и ASI7214X обрабатывают данные 100 тысяч пользователей. В памяти терминалов распознавания лиц сохраняются все событиях о проходах через данный терминал. Наибольшее количество событий в памяти позволяет строить отчеты за максимально долгий выбранный период времени.

Самый большой объем журнала событий у терминалов Face Station 2 и FaceLite 5 млн. У ProfaceX 1 млн. Терминалы ASI7223X-A и ASI7214X вмещают по 300 000 событий. Объем журнала SpeedFace V5L составляет 200 000 событий, у DS-K1T331W 150 000. У терминалов FaceDepot 7А и Facedepot 7 В и DS-K1T606MF 100 000 событий. Самый скромный объем памяти у терминала DS-K1T8105E всего 50 000 событий.

Языковой интерфейс


Не все терминалы распознавания лиц, представленные на российском рынке, имеют русскоязычный интерфейс, поэтому его наличие может являться важным фактором выбора.
Русскоязычный интерфейс доступен в терминалах ProFace X, SpeedFace V5L. В терминале Face Station 2 русскоязычная прошивка предоставляется по запросу. Терминал Face Station 2 имеет англоязычный интерфейс. DS-K1T331W поддерживает английский, испанский и арабский языки, русскоязычный интерфейс пока не доступен.

Габариты


Самыми крупными и тяжелыми в нашем обзоре являются терминалы Dahua.
ASI7223X-A 428X129X98 мм, вес 3 кг.
ASI7214X 250,6Х129Х30,5 мм, вес 2 кг.
Далее идет FaceDepot-7A с его весом в 1,5 кг и габаритами 301х152х46 мм.
Самым легким и компактным терминалом в нашем обзоре является Suprema FaceLite его габариты составляют 80x161x72 мм при весе в 0,4 кг.

Габариты терминалов Hikvision:
DS-K1T606MF 281X113X45
DS-K1T8105E 190X157X98
DS-K1T331W 120X110X23

Габариты терминалов Zkteco:
FaceDepot-7B 210X110X14 при весе 0,8 кг
ProfaceX 227X143X26 при весе 1 кг
SpeedFace V5L 203X92X22 при весе 0, 9 кг

Габариты терминала Suprema Face Station 2 141Х164Х125 при весе 0,7 кг.

Характеристики камеры


Терминал Proface X оснащен камерой 2MP WDR Low Light для распознавания лиц в условиях сильного внешнего освещения (50 000 люкс). Терминалы Face Station 2 и FaceLite оборудованы камерой CMOS 720x480 с инфракрасной подсветкой в 25 000 люкс, что позволяет им работать в условиях как низкой, так и высокой освещенности. Данные терминалы могут быть установлены под козырьком на открытом воздухе во избежание сильной засветки. Терминалы Hikvision и Dahua оснащены камерами на 2MP с двойным объективом и WDR, что позволяет получать четкое изображение в условиях различной освещенности. Терминалы FaceDepot 7А, Facedepot 7 В, SpeedFace V5L оснащены камерой
2MP.

Интеграция с турникетами




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

Из песочницы Создание нейросети по распознаванию лиц на фотографиях из Вконтакте

22.07.2020 12:20:51 | Автор: admin


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

Введение


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

План


  1. Получить ссылки на фотографии из беседы
  2. Скачать фотографии
  3. Написание нейросети

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

1. Получение ссылок на фотографии


Так мы хотим получить все фотографии с беседы нам подходит метод messages.getHistoryAttachments, который возвращает материалы диалога или беседы.
С 15 февраля 2019 Вконтакте запретил доступ к messages, для приложений не прошедших модерацию. Из вариантов обхода могу предложить vkhost, который поможет получить токен от сторонних мессенджеров

С полученным токеном на vkhost можем, собирать нужный нам запрос к API, при помощи Postman. Можно конечно и без него всё заполнить ручками, но для наглядности будем использовать его



Заполняем параметры:
  • peer_id идентификатор назначения
    Для беседы: 2000000000 + id беседы (можно увидеть в адресной строке).
    Для пользователя: id пользователя.
  • media_type тип материалов
    В нашем случае photo
  • start_from смещение, для выборки нескольких элементов.
    Пока что оставим пустым
  • count количество получаемых объектов
    Максимум 200, столько и будем использовать
  • photo_sizes флаг для возвращение всех размеров в массиве
    1 или 0. Мы используем 1
  • preserve_order флаг указывающий нужно ли возвращать вложения в оригинальном порядке
    1 или 0. Мы используем 1
  • v версия vk api
    1 или 0. Мы используем 1

Заполненные поля в Postman



Переходим к написанию кода
Для удобства весь код будет разбит на несколько раздельных скриптов

Будет использовать модуль json (для декодирования данных) и библиотеку requests (чтобы делать http запросы)

Листинг кода если в беседе/диалоге менее 200 фотографий

import jsonimport requestsval = 1 # Переменная для счётчикаFin = open("input.txt","a") # Создаём файл для записи ссылок# Отправляем GET запрос на API и записываем ответ в responseresponse = requests.get("https://api.vk.com/method/messages.getHistoryAttachments?peer_id=2000000078&media_type=photo&start_from=&count=10&photo_size=1&preserve_order=1&max_forwards_level=45&v=5.103&access_token=ВАШ_ТОКЕН")items = json.loads(response.text) # Считываем ответ от сервера в формате JSON# Так как по GET запросу сервер возвращает в каждом элементе массив с картинкой в разных размерах, будем перебирать всё цикломfor item in items['response']['items']: # Перебираем массив items    link = item['attachment']['photo']['sizes'][-1]['url'] # Записываем самый последний элемент, так как он самого максимального расширения    print(val,':',link) # В консоли выводим лог по проделанной работе    Fin.write(str(link)+"\n") # Записываем новую строку в файл    val += 1 # Увеличиваем значение счётчика

Если же фотографий более 200

import jsonimport requestsnext = None # Переменная в которую будем записывать ключ смещенияdef newfunc():    val = 1 # Переменная для счётчика    global next    Fin = open("input.txt","a") # Создаём файл для записи ссылок    # Отправляем GET запрос на API и записываем ответ в response    response = requests.get(f"https://api.vk.com/method/messages.getHistoryAttachments?peer_id=2000000078&media_type=photo&start_from={next}&count=200&photo_size=1&preserve_order=1&max_forwards_level=44&v=5.103&access_token=ВАШ_ТОКЕН")    items = json.loads(response.text) # Считываем ответ от сервера в формате JSON    if items['response']['items'] != []: # Проверка наличия данных в массиве        for item in items['response']['items']: # Перебираем массив items            link = item['attachment']['photo']['sizes'][-1]['url'] # Записываем самый последний элемент, так как он самого максимального расширения            print(val,':',link) # Лог перебора фотографий            val += 1 # Увеличиваем значение счётчика            Fin.write(str(link)+"\n") # Записываем новую строку в файл        next = items['response']['next_from'] # Записываем ключ для получения следующих фотографий        print('dd',items['response']['next_from'])        newfunc() # Вызываем функцию    else: # В случае отсутствия данных        print("Получили все фото")newfunc()

Ссылки получили пора качать

2. Скачивание изображений


Для скачивания фотографий используем библиотеку urllib

import urllib.requestf = open('input.txt') # Наш файл с ссылкамиval = 1 # Переменная для счётчикаfor line in f: # Перебираем файл построчно    line = line.rstrip('\n')    # Скачиваем изображение в папку "img"    urllib.request.urlretrieve(line, f"img/{val}.jpg")    print(val,':','скачан') # В логи выводим сообщение о загрузке    val += 1 # Увеличиваем счётчикprint("Готово")

Процесс загрузки всех изображений не самый быстрый, тем более если фотографий 8330. Место под это дело тоже требуется, если фотографий по количеству как у меня и более, рекомендую освободить под это 1,5 2 Гб



Черновая работа закончена, теперь можно приступать к самому интересному написанию нейросети

3. Написание нейросети


Просмотрев много различных библиотек и вариантов, было решено использовать библиотеку
Face Recognition

Что умеет ?


Из документации рассмотрим самые основные возможности

Поиск лиц на фотографиях
Может найти любое количество лиц на фото, даже справляется с размытыми



Идентификация лиц на фотографии
Может распознать кому принадлежит лицо на фотографии



Для нас самый подходящим способом будет являться идентификация лиц

Подготовка


Из требований к библиотеке необходим Python 3.3+ или Python 2.7
По поводу библиотек будет использоваться выше упомянутая Face Recognition и PIL для работы с изображениями.

Официально библиотека Face Recognition не поддерживается на Windows, но у меня всё заработало. С macOS и Linux всё работает стабильно.


Объяснение происходящего


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



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

Ну и собственно сам код:

import face_recognitionfrom PIL import Image # Библиотека для работы с изображениямиfind_face = face_recognition.load_image_file("face/sergey.jpg") # Загружаем изображение нужного человекаface_encoding = face_recognition.face_encodings(find_face)[0] # Кодируем уникальные черты лица, для того чтобы сравнивать с другимиi = 0 # Счётчик общего выполненияdone = 0 # Счётчик совпаденийnumFiles = 8330 # Тут указываем кол-во фотоwhile i != numFiles:    i += 1 # Увеличиваем счётчик общего выполнения    unknown_picture = face_recognition.load_image_file(f"img/{i}.jpg") # Загружаем скачанное изображение    unknown_face_encoding = face_recognition.face_encodings(unknown_picture) # Кодируем уникальные черты лица    pil_image = Image.fromarray(unknown_picture) # Записываем изображение в переменную    # Проверяем нашла ли нейросеть лицо    if len(unknown_face_encoding) > 0: # Если нашли лицо        encoding = unknown_face_encoding[0] # Обращаемся к 0 элементу, чтобы сравнить        results = face_recognition.compare_faces([face_encoding], encoding) # Сравниваем лица        if results[0] == True: # Если нашли сходство            done += 1 # Увеличиваем счётчик общего выполнения            print(i,"-","Нашли нужного человека !")            pil_image.save(f"done/{int(done)}.jpg") # Сохраняем фото с найденным человеком        else: # Если не нашли сходство            print(i,"-","Не нашли нужного человека!")    else: # Если не нашли лицо        print(i,"-","Лицо не найдено!")

Также есть возможность прогонять всё по глубинному анализу на видеокарте, для этого надо добавить параметр model= cnn и изменить фрагмент кода для изображения с котором хотим искать нужного человека:

    unknown_picture = face_recognition.load_image_file(f"img/{i}.jpg") # Загружаем скачанное изображение    face_locations = face_recognition.face_locations(unknown_picture, model= "cnn") # Подключаем ускорение GPU    unknown_face_encoding = face_recognition.face_encodings(unknown_picture) # Кодируем уникальные черты лица

Результат


Без GPU. По времени нейросеть перебрала и отсортировала 8330 фотографий за 1 час 40 минут и при этом нашла 142 фотографии из них 62 с изображением нужного человека. Конечно бывали ложные срабатывания, на мемы и других людей.

C GPU. Времени на обработку заняло гораздо больше, 17 часов и 22 минуты и нашла 230 фотографий из которых 99 нужный нам человек.

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

Также можете скачать весь исходный код с github
Подробнее..

Лицевые анимации из двумерных видео

29.11.2020 04:09:36 | Автор: admin
КликбейтКликбейт

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

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

Введение

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

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

  • Создание маркеров на основе захвата движения по точкам или отметинам на лице исполнителя

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

  • Безмаркерные методы с использованием различных типов камер

    • Методы, использующие различные способы создания карты глубин, такие, как камеры Kinect или Intel Real Sense

  • Звуко-управляемые методы

    • Методы, использующие скрытые марковские модели или нейронные сети для преобразования звуковых параметров в поток управляющих параметров для модели лица.

  • Анимация по ключевым кадрам

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

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

  1. Автоматизированность

  2. Отсутствие необходимости в использовании дорогостоящего оборудования

  3. Относительная точность передачи положения лица в пространства

  4. Отсутствие необходимости использования дополнительных маркеров

Выборка

Чтобы собрать что-то дельное, необходимо составить математическую модель того, что мы собираемся делать. Ну или хотя бы понять, как вообще мы будем определять качество итогового результата? Поэтому

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

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

Так выглядят три последовательных кадра и параметры, которые мы оцениваем на них. Данные собираются автоматически в процессе построения опорных точек. Основные параметры, которые я собрал включают в себя:

  • Rele - расстояние между левым и правым глазом, показывающее искажение верхней части лица

  • Ren - расстояние между правым глазом и носом, показывающее искажение правой скулы

  • Len - расстояние между левым глазом и носом, показывающее искажение левой скулы

  • Chin 1-8 - набор расстояний, состоящих из расстояний между левой и правой частью лица, показывающий общее искажение лица

Поговорим о том, что эти параметры собой представляют.

Параметр rele

Пусть (x,y,z) координаты центра левого и правого глаза вычисляются последовательно для каждого кадра по следующим формулам:

x = \frac{\sum_{i=m}^{n}{a_{i_0}+a} }{n+1}\\y = \frac{\sum_{i=m}^{n}{a_{i_1}+a} }{n+1}\\ z = \frac{\sum_{i=m}^{n}{a_{i_2}+a} }{n+1}

Список значений rele хранит в себе длину вектора AB, где A-координата центра левого глаза, B-координата центра правого глаза соответственно, а равно, расстояние между центрами левого и правого глаза. Пусть каждое значение списка rele вычисляется по следующей формуле для каждого кадра:

rele_i = \sqrt{(x_{left}-x_{right})^2+(y_{left}-y_{right})^2+(z_{left}-z_{right})^2}

Тогда значение rele для всех кадров, пропорционально их количеству будем вычислять по следующей формуле, где - количество кадров анимации:

rele = \frac{ \sum_{i=m}^{n}{\frac{rele_i}{rele_0}} }{a}

Значение rele показывает среднее изменение длины вектора AB - расстояния между центрами правого и левого глаза. Из чего можем провести следующую зависимость:

Чем меньше значение функции rele для видеофайла, тем меньше изменялись основные пропорции лица, а равно, тем (предположительно) лучше качество конечной анимации

Параметр len

Пусть (x,y,z) координаты центра левого глаза вычисляются последовательно для каждого кадра по следующим формулам:

x = \frac{\sum_{i=m}^{n}{a_{i_0}+a} }{n+1}\\y = \frac{\sum_{i=m}^{n}{a_{i_1}+a} }{n+1}\\ z = \frac{\sum_{i=m}^{n}{a_{i_2}+a} }{n+1}

Список значений len хранит в себе длину вектораAB, где A-координата центра левого глаза, B-координата конца носа, а равно, расстояние между центром левого глаза и концом носа. Пусть каждое значение списка len вычисляется по следующей формуле для каждого кадра:

len_i = \sqrt{(x_{left}-x_{nose})^2+(y_{left}-y_{nose})^2+(z_{left}-z_{nose})^2}

Тогда значение len для всех кадров, пропорционально их количеству будем вычислять по следующей формуле, где - количество кадров анимации:

len = \frac{ \sum_{i=m}^{n}{\frac{len_i}{len_0}} }{a}

Значение len показывает среднее изменение длины вектора AB - расстояния между центром левого глаза и носом. Из чего можем провести следующую зависимость:

Чем меньше значение функции len для видеофайла, тем меньше изменялось расстояние между левым глазом и носом, а равно один из основных параметров лица, тем (предположительно) лучше качество конечной анимации

Параметр ren будем вычислять по тем же формулам, внеся соответсвующие изменения.

Список параметров chin0-chin7

Список параметров chin0 - chin7 показывает изменение длины вектора AB между каждыми двумя точками, относящимися к нижней дуги лица (точки 0-16).

Список значений chini хранит в себе длины векторов AB, где A-точка лежащая на левой полудуге, B-точка лежащая на правой полудуге соответственно, а равно, расстояние левой и правой стороной лица в точке i. Пусть каждое значение списка chini вычисляется по следующей формуле для каждого кадра:

chin_i = \sqrt{(x_{chin-left}-x_{chin-right})^2+(y_{chin-left}-y_{chin-right})^2+(z_{chin-left}-z_{chin-right})^2}

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

chin = \frac{ \sum_{i=m}^{n}{\frac{chin_i}{n}} }{a}

Значение chin показывает среднее изменение длины вектора AB - расстояния между двумя точками на левой и правой стороне нижней дуги лица. Из чего можем провести следующую зависимость:

Чем меньше значение функции chin{0-7} для видеофайла, тем меньше изменялось расстояние между двумя половинами лица, а равно один из основных параметров лица, тем (предположительно) лучше качество конечной анимации

Все эти метрики позволяют относительно точно отследить качество итоговой анимации не только по итогу, но ещё и покадрово как-то вот так:

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

Этапы формаирования косточек и анимацииЭтапы формаирования косточек и анимации

Результаты работы скрипта

Кошмар разКошмар раз
Кошмар дваКошмар два
Кошмар триКошмар триНормальная табличка сюда не влезла, наслаждайтесь 10/10 шакаловНормальная табличка сюда не влезла, наслаждайтесь 10/10 шакалов

Давайте подробнее рассмотрим "Кошмар три"

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

Ну и вот итог всего пути, который мы прошли. От точек в пространстве, до сносной анимации:

Подводим итоги

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

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

Подробнее..

Нейросеть научилась генерить лица, привлекательные для мозга

29.04.2021 14:14:31 | Автор: admin

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

Мишель Спейп и его коллеги из Хельсинского университета смогли обучить нейронные сети создавать изображения мужских и женских лиц, отвечающих индивидуальным предпочтениям людей. Причем, точность предсказания того, красив ли изображенный человек, составила свыше 80%.

Генеративно-состязательная нейронная сеть (Generative adversarial network, сокращенно GAN) научилась выявлять индивидуальные стандарты красоты при помощи ЭЭГ участников эксперимента. Статья об этом опубликована в журнале IEEE Transactions in Affective Computing.

Алгоритм GAN представляет собой сочетание двух нейросетей генератора и дискриминатора. Задача генератора создавать образы заданной категории, задача дискриминатора попытаться распознать данный образ.

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

А алгоритм Everybody Dance Now позволяет на основе видео с танцором сгенерировать фейковую запись, на которой другой человек будет повторять те же движения.

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

Однако создать изображение лица, которое будет красивым для конкретного человека, дол.гое время не удавалось.

Авторы исследования из Хельсинкского университета считают, что субъективно привлекательность зависит от неявной обработки определенных черт. Причем, у каждого человека они свои. По их мнению, генеративно-состязательные нейронные сети потенциально способны смоделировать субъективные предпочтения, не опираясь на заранее заданные параметры.

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

Как сообщает N+1, в исследовании финских ученых, объединяющем информатику и психологию, приняли участие 30 сотрудников и студентов Хельсинкского университета.

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

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

Спустя два месяца люди, принимавшие участие в эксперименте, пришли, чтобы оценить его результаты. Пришедшим продемонстрировали матрицу из двух рядов по 12 картинок. Среди изображений, созданных нейросетью, были непривлекательные и нейтральные лица, а также лица предположительно симпатичные определённому человеку. Участникам предложили нажимать на изображения, которые они считали красивыми, а также оценить их по шкале от 1 (очень непривлекательное) до 5 (очень привлекательное).

Эксперимент оказался успешным! 86,7% созданных нейросетью привлекательных изображений изображений, подтвердились предпочтениями людей. Однако, участникам понравились 20% изображений, которые GAN создала в качестве непривлекательных. То есть по результатам исследования, появились ложноотрицательные результаты.

Более высокие рейтинги получили изображения, которые нейросеть создала как привлекательные, в сравнении с нейтральными и непривлекательными. То есть была найдена такая структура данных, которая позволяет разделять реакции мозга на привлекательные и непривлекательные лица. Причем, речь идет о высокой точности совпадений с реальными реакциями людей (83,33%).

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

Эксперимент доказал, что использование данных ЭЭГ для управления генеративно-состязательными нейронными сетями действенный инструмент для интерактивного генерирования информации.

Возможно, в недалеком будущем в каждом доме появится свой Джарвис эмпатичный и с чувством юмора. Но уже сейчас можно создать чат-бота, понимающего обычную письменную речь, а не отдельные команды. Мы говорили об этом еще в доковидные времена на сибирском HighLoad++.

КонференцияHighLoad++ 2021пройдет уже 17 и 18 мая в Москве, в Крокус-экспо. У нас готоворасписание, и вы уже сегодня можете запланировать активности!

Билеты можно купитьздесь. С 1 мая цена на них станет выше.

Хотите бесплатно получить материалы мини-конференции Saint HighLoad++ 2020?Подписывайтесьна нашу рассылку.

Подробнее..

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, то с её помощью мы можем добавить идентификацию лиц в непрерывном видеопотоке, определять пол, возраст и эмоции. Эти возможности дают простор для множества экспериментов. А мы на своём опыте можем сказать: не бойтесь экспериментов и бросайте вызов себе!
Подробнее..

Перевод - recovery mode Технология распознавания лиц тайная история

14.08.2020 12:15:32 | Автор: admin
Шестьдесят лет назад Вуди Бледсо (Woody Bledsoe) сын земледельца изобрёл технологию идентификации лиц. Но свидетельство о его причастности к открытию практически исчезло.

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

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

Но в начале карьеры Бледсо увлечённо искал возможность научить машины распознавать лица недооценённую тогда, но потенциально мощную человеческую способность. Это были первые исследования по идентификации лиц (1960 года), и работа профессора привлекла интерес спецслужб США. Главные инвесторы Вуди, скорее всего, были подставными компаниями ЦРУ.

Распознавание лиц: благо и ящик Пандоры


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

Так, при помощи этой технологии в Китае правительство отслеживает представителей уйгурского этнического меньшинства, сотни тысяч которых поместили в лагеря для политических заключённых. А в США, по данным The Washington Post, Иммиграционная и таможенная полиция и ФБР проводит цифровой розыск: ищет подозреваемых в государственных базах данных водительских удостоверений иногда без предварительного обращения в суд.

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

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

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

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

Как всё начиналось. Метод кортежей


Вудро Вильсон (Вуди) Бледсо родился в 1921 году в многодетной семье земледельца-издольщика из Оклахомы. Был десятым ребёнком в семье и сколько себя помнил всегда помогал отцу по хозяйству. Обладал математическим складом ума. Окончил среднюю школу. Три месяца проучился в Университете Оклахомы, после чего Вуди призвали в армию в канун Второй мировой войны.

После войны Вуди изучал математику в Университете Юты, а затем уехал в Беркли для получения степени доктора наук. Закончив аспирантуру, Вуди занимался исследованиями в области ядерного оружия в правительственной корпорации Sandia в Нью-Мексико вместе с такими светилами, как Станислав Улам (Stanislaw Ulam), который участвовал в создании водородной бомбы.

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

Вуди Бледсо и его коллега Ибен Браунинг (Iben Browning) изобретатель-эрудит, авиаинженер и биофизик придумали метод, который впоследствии стал известен как метод кортежей (n-tuple).

Учёные начали с проецирования напечатанного символа скажем, буквы Q на прямоугольную сетку из клеток наподобие разлинованного листа бумаги. Каждой клетке-ячейке присваивался двоичный номер в зависимости от наличия или отсутствия в ней части символа: 0 для пустой клетки, 1 для заполненной. Затем ячейки случайным образом группировались в упорядоченные пары, как наборы координат. Теоретически группы могли включать любое количество ячеек, отсюда и название метода. Далее при помощи нескольких математических действий система присваивала сетке символа уникальное значение. А при столкновении с новым символом сетка этого символа сравнивалась с другими в базе данных до тех пор, пока не находилось ближайшее совпадение.

Суть метода состояла в том, что он позволял распознавать множество вариантов одного и того же знака: большинство Q, как правило, получали довольно схожие результаты по сравнению с другими Q. Лучше всего процесс работал с любым шаблоном, а не только с текстом. По словам Роберта С. Бойера (Robert S. Boyer), математика и давнего друга Вуди, метод кортежей помог определить область распознавания шаблонов. Это был один из первых шагов к вопросу: Как запрограммировать машину делать то, что делают люди?".

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

Спустя годы он вспоминал дикое волнение, которое испытывал, формулируя навыки для искусственного разума:


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




Исследования в компании Panoramic Research Incorporated


В 1960 году Вуди вместе с Ибеном Браунингом и ещё одним коллегой из Sandia основал компанию Panoramic Research Incorporated (Panoramic). Сначала они располагались в небольшом помещении в Пало-Альто, который ещё не был известен как сердце Кремниевой долины. В то время большинство компьютеров массивных устройств, которые хранили данные на перфокартах или магнитных лентах, размещались в офисах крупных компаний и правительственных лабораториях. Компания Вуди не могла себе позволить купить компьютер, поэтому учёные арендовали вычислительное время на такой машине у своих соседей, часто поздно вечером, когда оно было дешевле.

Бизнес Panoramic заключался в том, чтобы тестировать идеи, которые, как мы надеялись, перевернут мир.

По словам Нельса Уинклесса (Nels Winkless), писателя и консультанта, который участвовал в нескольких проектах Panoramic, а позже стал одним из основателей журнала Personal Computing, их задача заключалась в том, чтобы делать то, что другие люди находят слишком глупым.

Изобретения некоторых исследователей Panoramic получили широкую известность. Например, Хелен Чан Вульф (Helen Chan Wolf), пионер в программировании роботов, работала над созданием робота Шейки (Shakey the Robot). По мнению Института инженеров по электротехнике и радиоэлектронике, это первый в мире робот, воплощающий в себе искусственный интеллект.

Panoramic тщетно пыталась найти финансирование. Вуди сделал всё возможное для презентации технологии распознавания символов, в том числе представив изобретение Обществу справедливого страхования жизни и журналу McCall's. Но контракт так и не заключили.

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

Если в бумагах Вуди Бледсо когда и были упоминания о ЦРУ, то, скорее всего, они были уничтожены в 1995 году. Но фрагменты сохранившихся материалов явно свидетельствуют о том, что в течение многих лет компания Вуди работала с подставными компаниями ЦРУ. Нельс Уинклесс, который приятельствовал с командой Panoramic, говорит, что компания, скорее всего, была создана благодаря финансированию агентства. Никто никогда не говорил мне об этом прямо, вспоминает Уинклесс, но так оно и было.

Согласно данным сайта the Black Vault, который занимается запросами по Закону о свободном доступе к информации, компания Panoramic Research Incorporated входила в число 80 организаций, работавших над проектом MK-Ultra. Это печально известная программа ЦРУ по контролю разума, где применялись психологические пытки без согласия людей. Через подставной исследовательский фонд the Medical Sciences Research Foundation компании Panoramic поручили заниматься подпроектами по изучению бактериальных и грибковых токсинов и дистанционному управлению деятельностью отдельных видов животных.

Дэвид Х. Прайс (David H. Price), антрополог из Университета Сен-Мартина, считал, что Вуди и его коллеги также получали деньги от Общества по изучению экологии человека. От имени этого общества ЦРУ предоставляло гранты учёным, чьи работы могли улучшить методы допроса, которые использовались агентством, или выступить в качестве прикрытия для таких исследований.

Но проведение самых значимых исследований компании Panoramic обеспечила другая фиктивная компания the King-Hurley Research Group (King-Hurley). Согласно серии исков, поданных в 1970 годах, ЦРУ использовало эту исследовательскую группу для закупки самолётов и вертолётов для секретных военно-воздушных сил агентства, известных как Air America. Некоторое время King-Hurley также финансировала психофармакологические исследования в Стэнфорде.

В начале 1963 года King-Hurley принимала разного рода презентации идей только от Вуди Бледсо. Он предложил провести исследование для определения целесообразности создания упрощённой машины для распознавания лиц. Опираясь на их с Браунингом работу по методу кортежей, Вуди хотел научить систему распознавать 10 лиц. То есть он планировал использовать базу данных из 10 фотографий разных людей и узнать, сможет ли машина идентифицировать новые фотографии каждого из них. Вскоре можно будет увеличить число людей до тысяч, писал Вуди. В течение месяца King-Hurley выдал ему разрешение на старт работ.

Первые эксперименты в области распознавания лиц


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

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

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

И вряд ли можно было бы с уверенностью утверждать, что их компьютеры справятся с этой задачей. Одной из основных машин была CDC 1604 со 192 КБ оперативной памяти примерно в 21 000 раз меньше, чем у обычного современного смартфона.

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

Работа над оцифровкой изображений проходила следующим образом. Исследователь снимал чёрно-белые фотографии участников проекта на 16-миллиметровую плёнку. Затем использовал сканирующее устройство, которое разработал Браунинг, чтобы преобразовать каждый снимок в десятки тысяч точек данных. Каждая точка должна была иметь значение интенсивности света в диапазоне от 0 (самая тёмная) до 3 (самая светлая) в определённом месте на снимке. Получалось слишком много точек для единовременной обработки компьютером, поэтому исследователь написал программу NUBLOB, которая нарезала изображение на образцы случайного размера и вычисляла для каждого уникальное значение наподобие тех, что присваивались по методу кортежей.

Над наклоном головы работали Вуди, Хелен Чан Вульф и ещё один исследователь. Сначала учёные нарисовали серию пронумерованных маленьких крестиков на левой стороне лица участника эксперимента от вершины лба до подбородка. Затем сделали два портрета, на одном из которых человек смотрел вперёд, а на другом повернулся на 45 градусов. Проанализировав местоположение крестиков на этих двух изображениях, экстраполировали данные на снимок лица с поворотом на 15 или 30 градусов. Загружали в компьютер чёрно-белую картинку размеченного лица, а на выходе получали автоматически крутящийся портрет страшноватый, точечный и удивительно точный.

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

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

Человеко-машинный подход к распознаванию лиц


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

Система, которую он предложил, была похожа на метод французского криминолога Альфонса Бертильона, который он создал в 1879 году. Бертильон описывал людей на основе 11 физических измерений, включая длину левой ноги и длину от локтя до конца среднего пальца. Идея состояла в том, что если провести достаточно измерений, то каждый человек станет уникальным. Метод был трудоёмким, но работал: при помощи него в 1897 году, задолго до широкого распространения дактилоскопирования, французские жандармы идентифицировали серийного убийцу Жозефа Ваше.

На протяжении 1965 года Panoramic пробовала создать полностью автоматизированную систему Бертильона для идентификации лиц. Команда пыталась разработать программу, которая могла бы определять носы, губы и другое при помощи светлых и тёмных участков на фотографии. Но их постигла неудача.

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

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

Машина научилась сопоставлять каждый комплект измерений с фотографией. Результаты были скромными, но неоспоримыми: Вульф и Вуди доказали, что система Бертильона теоретически работоспособна.

Их следующим шагом, в конце 1965 года, было создание более масштабной версии того же эксперимента, чтобы сделать человека более эффективным в их системе человек-машина. На деньги King-Hurley они приобрели планшет RAND устройство стоимостью 18 000 долларов, которое выглядело как планшетный сканер изображений, а работало как iPad. При помощи стилуса исследователь рисовал на планшете и на выходе получал компьютерное изображение относительно высокого разрешения.

Через планшет RAND провели новую партию фотографий, подчёркивая стилусом ключевые элементы лица. Этот процесс хотя и был сложным, но проходил гораздо быстрее, чем раньше: данные ввели примерно для 2 000 снимков, включая как минимум два изображения каждого лица. В час обрабатывали порядка 40 снимков.

Даже при такой, более крупной выборке команда Вуди с трудом преодолевала обычные препятствия.

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

При попытке сопоставить фотографию Вуди 1945 года с фотографией 1965 года система сбивалась с толку. Она не видела большого сходства между молодым человеком с широкой улыбкой и густыми тёмными волосами и человеком более старшего возраста с мрачным выражением лица и поредевшей шевелюрой.


Фото Вуди Бледсо из исследования 1965 года. Фотограф: Dan Winters

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

В марте 1965 года за 50 лет до того, как Китай начал использовать совпадение паттернов лица для идентификации этнических уйгуров в провинции Синьцзян Вуди предложил Управлению перспективных исследовательских проектов (Advanced Research Projects Agency ARPA) при Министерстве обороны США поддержать Panoramic в изучении использования черт лица для определения расового происхождения человека. Существует очень большое количество антропологических измерений людей со всего мира, которые принадлежат к различным расовым и экологическим группам, писал Вуди. Это обширное и ценное хранилище данных, которое было собрано с большим трудом и крупными затратами, но не использовано должным образом. Согласилась ли ARPA финансировать этот проект, остаётся неизвестным.

Вуди вкладывал в Panoramic тысячи долларов из собственных средств без гарантии их возврата. А тем временем его друзья из Техасского университета в Остине уговаривали его устроиться в университет, завлекая стабильной зарплатой. И в январе 1966 года Вуди покинул Panoramic. Вскоре после этого компания закрылась.

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

Самый успешный эксперимент Вуди Бледсо по распознаванию лиц


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

Как и прежде, финансирование проекта, судя по всему, поступило от правительства США. В документе 1967 года, рассекреченном ЦРУ в 2005 году, упоминается внешний контракт на систему распознавания лиц, что позволило бы в сто раз сократить время поиска.

Основным партнёром Вуди по проекту был Питер Харт (Peter Hart), инженер-исследователь Лаборатории прикладной физики Стэнфордского научно-исследовательского института. (Сейчас известный как SRI International. Институт отделился от Стэнфордского университета в 1970 году из-за разногласий в кампусе по поводу сильной зависимости института от военного финансирования.)

Вуди и Харт начали с базы данных из порядка 800 снимков по два снимка 400 взрослых мужчин европеоидной расы. Сфотографированные различались по возрасту и повороту головы. При помощи планшета RAND учёные зафиксировали 46 координат для каждой фотографии, в том числе пять значений для каждого уха, семь для носа и четыре для каждой брови. На базе предыдущего опыта Вуди по нормализации вариаций изображений применили математическое уравнение, чтобы повернуть головы в анфас. Затем, для учёта разницы в масштабах, увеличили или уменьшили каждое изображение до стандартного размера, где опорной метрикой было расстояние между зрачками.

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

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


Фотограф: Dan Winters

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

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

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

В 1970 году, через два года после окончания сотрудничества с Хартом, робототехник по имени Майкл Касслер сообщил Вуди, что Леон Хармон (Leon Harmon) из Bell Labs планирует провести исследование по распознаванию лиц. Меня возмущает, что это исследование второго рода будет опубликовано и окажется в итоге лучшей системой человекмашина", ответил Вуди. Мне кажется, что при условии усердной работы Леон будет отставать от нас где-то на 10 лет к 1975 году. Должно быть, Вуди был разочарован, когда несколько лет спустя исследование Хармона попало на обложку журнала Scientific American, в то время как его собственная, более продвинутая работа хранилась в запасниках.

Использование метода Вуди Бледсо в современной технологии распознавания лиц


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

В 1973 году японский учёный-программист Такэо Канаде (Takeo Kanade) совершил большой скачок в технологии распознавания лиц.

На основе базы данных из 850 оцифрованных фотографий со Всемирной выставки в Суите (Япония) в 1970 году Канаде разработал программу, которая могла извлекать черты лица нос, рот и глаза без участия человека. Канаде удалось осуществить мечту Вуди об исключении человека из системы человек-машина.

За эти годы пару раз Вуди использовал свои знания в области распознавания лиц.

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

Только за последние 10 лет технология распознавания лиц научилась работать с несовершенствами, говорит Анил К. Джейн (Anil K. Jain), учёный-программист Мичиганского государственного университета и соредактор Руководства по распознаванию лиц (Handbook of Face Recognition).

Почти все проблемы, с которыми сталкивался Вуди, отпали. Cегодня есть неисчерпаемый запас оцифрованных изображений. Через социальные сети вы можете получать столько снимков лица, сколько захотите, говорит Джейн. А благодаря достижениям в области машинного обучения, объёму памяти и вычислительной мощности компьютеры эффективно самообучаются. Учитывая несколько простых правил, они могут анализировать огромные объёмы данных и создавать шаблоны практически для чего угодно, начиная от человеческого лица и заканчивая пакетом чипсов никаких замеров с помощью планшета RAND или метода Бертильона больше не требуется.

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

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

И хотя современным системам на основе глубокого обучения программист не даёт явного указания идентифицировать носы и брови, поворот Вуди в этом направлении в 1965 году задал направление развития отрасли на десятилетия. Первые 40 лет главенствовал именно метод, основанный на выделении признаков, говорит Такэо Канаде, в настоящее время профессор Института робототехники Карнеги-Меллона.

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

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

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

В 2019 году в ходе тестирования программного обеспечения Rekognition от Amazon 28 игроков НФЛ были ошибочно определены как преступники. Спустя несколько дней Американский союз защиты гражданских свобод предъявил иск Министерству юстиции США, ФБР и Управлению по борьбе с наркотиками на предмет получения информации об использовании ими технологии распознавания лиц от Amazon, Microsoft и других компаний. В отчёте Национального института стандартов и технологий за 2019 год, который протестировал код более 50 разработчиков программного обеспечения для распознавания лиц, говорится, что белые мужчины неверно сопоставляются с преступниками реже, чем представители других групп. В 2018 году пара учёных выступила с резкой критикой: Мы считаем, что технология распознавания лиц самый опасный механизм наблюдения, когда-либо изобретённый.

Весной 1993 года из-за дегенеративного заболевания БАС речь Вуди ухудшилась. Но он продолжал преподавать в Техасском университете до тех пор, пока его речь не стала неразборчивой. Он продолжал и исследования в области автоматизированных рассуждений пока не перестал держать ручку. До конца оставаясь учёным, Вуди делал записи своей речи, чтобы отслеживать развитие болезни.

Вуди Бледсо умер 4 октября 1995 года. В некрологе не упоминалась его работа по распознаванию лиц. На фотографии в некрологе седовласый Вуди смотрит прямо в камеру и его лицо озаряет широкая улыбка.



Комментарий Елены Герасимовой, руководителя направления Аналитика и Data Science в Нетологии


Идеи Вуди Бледсо не имели коммерческого успеха, возможно, потому, что их время пришлось на одну из зим искусственного интеллекта. Было мало доверия к технологиям, не хватало мощностей для демонстрации впечатляющих результатов, а технологиями воссоздания мозга человека занимались в основном энтузиасты академик Андрей Николаевич Колмогоров, американский математик Джордж Цибенко и другие.

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


В 1998 году подходы к распознаванию рукописных цифр усовершенствовал Ян Лекун в его сети LeNet благодаря эволюции вычислительных мощностей, которых не было во времена исследований Вуди Бледсо

Технология распознавания лиц граничит с более продвинутой технологией генерации лиц, которая используется для, например, создания дипфейков и генерации лиц взрослых и детей, а также котиков, собак. Казалось бы, что проще сделать фото человека и загрузить его, условно, в электронный каталог одежды; или снять симпатичное видео с младенцем и игрушками; или научить нейронную сеть создавать изображение ребенка в одежде, интерьере или с игрушкой, которую мы планируем размещать в каталоге и таким образом демонстрировать? Ответ подскажет сумма инвестиций в компании разработчиков технологии создания фотореалистичных изображений только в США в 2019 году суммарный объём инвестиций составил более 500 млн долларов.



Генерация фотореалистичных изображений людей

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

  • Профессия Data Scientist. Премия Знак качества в номинации Подготовка профессионалов цифровой индустрии (2019).
  • Курс Deep Learning. На лендинге курса сможете познакомиться с примерами проектов в рамках курса и посмотреть, как проходит обучение.

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

Евросоюз хочет ограничить использование ИИ и систем распознавания лиц в угоду приватности

22.04.2021 02:17:37 | Автор: admin
Чиновники Евросоюза планируют ограничить использование распознавания лиц полицией и полностью запретить определённые типы систем искусственного интеллекта (ИИ). Это станет одним из самых значительных попыток наложить ограничения на применение ИИ.

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

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

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

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

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

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

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

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

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

Подробнее..

Категории

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

  • Имя: Макс
    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