Обнаружение лиц на видео: Raspberry Pi и Neural Compute Stick

    Около года назад компания Intel Movidius выпустила устройство для эффективного инференса сверточных нейросетей — Movidius Neural Compute Stick (NCS). Это устройство позволяет использовать нейросети для распознавания или детектирования объектов в условиях ограниченного энергопотребления, в том числе в задачах робототехники. NCS имеет USB-интерфейс и потребляет не более 1 ватта. В этой статье я расскажу об опыте использования NCS с Raspberry Pi для задачи обнаружения лиц в видео, включая как обучение Mobilenet-SSD детектора, так и его запуск на Raspberry.

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



    В своей первой статье я уже писал про обнаружение лиц с помощью NCS: тогда речь шла о YOLOv2 детекторе, который я конвертировал из формата Darknet в формат Caffe, а затем запускал на NCS. Процесс конвертирования оказался нетривиальным: поскольку эти два формата по-разному задают последний слой детектора, выход нейросети приходилось парсить отдельно, на CPU, с помощью куска кода из Darknet. Кроме того, этот детектор не удовлетворил меня как по скорости (до 5.1 FPS на моем ноутбуке), так и по точности — позже я убедился, что из-за чувствительности к качеству изображения от него сложно получить хороший результат на Raspberry Pi.

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

    Как работает Mobilenet-SSD детектор
    Начнем с Mobilenet. В этой архитектуре полная $3\times 3$ свертка (по всем каналам) заменяется на две легковесные свертки: сначала $3\times 3$ отдельно для каждого канала, а затем полная $1\times 1$ свертка. После каждой свертки используются BatchNorm и нелинейность (ReLU). Самая первая свертка сети, получающая на вход изображение, обычно оставляется полной. Эта архитектура позволяет значительно снизить сложность вычислений за счет небольшого снижения качества предсказаний. Есть и более продвинутый вариант, но его я пока не пробовал.

    SSD (Single Shot Detector) работает так: на выходы нескольких сверток энкодера навешиваются по два $1\times 1$ сверточных слоя: один предсказывает вероятности классов, другой — координаты ограничивающих рамок. Есть еще третий слой, который выдает координаты и положения дефолтных рамок на текущем уровне. Смысл такой: выход любого слоя естественным образом разбит на ячейки; ближе к концу нейросети их становится все меньше (в данном случае, из-за сверток с stride=2), а поле видимости каждой ячейки увеличивается. Для каждой ячейки на каждом из нескольких выбранных слоев мы задаем несколько дефолтных рамок разного размера и с разными соотношениями сторон, а дополнительные сверточные слои используем, чтобы поправить координаты и предсказать вероятности классов для каждой такой рамки. Поэтому SSD детектор (так же, как и YOLO) всегда рассматривает одинаковое число рамок. Один и тот же объект может детектироваться на разных слоях: во время обучения сигнал посылается всем рамкам, которые достаточно сильно пересекаются с объектом, а во время применения детекции объединяются с помощью non maximum suppression (NMS). Финальный слой объединяет детекции со всех слоев, считает их полные координаты, отсекает по порогу вероятности и производит NMS.

    Обучение детектора


    Архитектура


    Код для обучения детектора расположен здесь.

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

    Исходный проект включал скрипт gen.py, который буквально собирал .prototxt файл модели, подставляя входные параметры. Я перенес его в свой проект, немного расширив функционал. Этот скрипт позволяет сгенерировать четыре типа конфигурационных файлов:

    • train: на входе — обучающая LMDB база, на выходе — слой с подсчетом функции потерь и ее градиентов, есть BatchNorm
    • test: на входе — тестовая LMDB база, на выходе — слой с подсчетом качества (mean average precision), есть BatchNorm
    • deploy: на входе — изображение, на выходе — слой с предсказаниями, BatchNorm отсутствует
    • deploy_bn: на входе — изображение, на выходе — слой с предсказаниями, есть BatchNorm

    Последний вариант я добавил позже, чтобы в скриптах можно было загружать и преобразовывать сетку с BatchNorm, не трогая LMDB базы — иначе при отсутствии базы ничего не работало. (Вообще, мне кажется странным, что в Caffe источник данных задается в архитектуре сети — это как минимум не очень практично).

    Как выглядит архитектура сети (коротко)
    • Вход: $300\times 300\times 3$
    • Полная свертка conv0: 32 канала, stride=2
    • Mobilenet свертки conv1 — conv11: 64, 128, 128, 256, 256, 512… 512 каналов, некоторые имеют stride=2
    • Слой детекций: $19\times 19\times 512$
    • Mobilenet свертки conv12, conv13: 1024 канала, conv12 имеет stride=2
    • Слой детекций: $10\times 10\times 1024$
    • Полные свертки conv14_1, conv14_2: 256, 512 каналов, у первой kernel_size=1, у второй stride=2
    • Слой детекций: $5\times 5\times 512$
    • Полные свертки conv15_1, conv15_2: 128, 256 каналов, у первой kernel_size=1, у второй stride=2
    • Слой детекций: $3\times 3\times 256$
    • Полные свертки conv16_1, conv16_2: 128, 256 каналов, у первой kernel_size=1, у второй stride=2
    • Слой детекций: $2\times 2\times 256$
    • Полные свертки conv17_1, conv17_2: 64, 128 каналов, у первой kernel_size=1, у второй stride=2
    • Слой детекций: $1\times 1\times 128$
    • Финальный слой Detection output


    Архитектуру сети я слегка подкорректировал. Список изменений:

    • Очевидно, число классов сменилось на 1 (не считая фона).
    • Ограничения на соотношение сторон вырезаемых патчей при обучении: изменились с $ [0.5,2.0]$ на $ [0.7,1.4]$ (я решил немного упростить задачу и не обучаться на слишком растянутых картинках).
    • Из дефолтных рамок остались только квадратные, по две на каждую ячейку. Их размеры я сильно уменьшил, поскольку лица существенно меньше, чем объекты в классической задаче детектирования объектов.

    Caffe рассчитывает размеры дефолтных рамок так: имея минимальный размер рамки $s$ и максимальный $L$, она создает маленькую и большую рамки с размерами $s$ и $\sqrt{sL}$. Поскольку мне хотелось детектировать как можно более мелкие лица, я рассчитал полный stride для каждого слоя детекций и приравнял минимальный размер рамки к нему. При таких параметрах маленькие дефолтные рамки будут располагаться вплотную друг к другу и не будут пересекаться. Так у нас хотя бы есть гарантия, что пересечение с объектом будет существовать для какой-то рамки. Максимальный размер я установил вдвое больше. Для слоев conv16_2, conv17_2 я выставил размеры на глаз, одинаковыми. Таким образом, $s,L$ для всех слоев составили: $(16,32),(32,64),(64,128),(128,214),(214,300),(214,300)$

    Как выглядят некоторые дефолтные рамки (шум для наглядности)


    Данные


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

    Для обучения я использовал не все данные: я выкинул слишком маленькие лица (меньше шести пикселей или меньше 2% ширины изображения), выкинул все изображения с соотношением сторон меньше 0.5 или больше 2, выкинул все изображения, помеченные как «размытые» в датасете WIDER, поскольку они соответствовали по большей части совсем мелким лицам, и мне надо было хоть как-то выровнять соотношение мелких и крупных лиц. После этого я сделал все рамки квадратными, расширив наименьшую сторону: я решил, что меня не очень интересуют пропорции лица, а задача для нейросети немного упрощается. Также я выкинул все черно-белые картинки, которых было немного, и на которых скрипт сборки базы данных падает.

    Чтобы использовать их для обучения и тестирования, надо собрать из них LMDB базу. Как это делается:

    • Для каждого изображения создается разметка в .xml формате.
    • Создается файл train.txt со строками вида "path/to/image.png path/to/labels.xml", такой же создается для test.
    • Создается файл test_name_size.txt со строками вида "test_image_name height width"
    • Создается файл labelmap.prototxt с числовыми соответствиями меткам


    Запускается скрипт ssd-caffe/scripts/create_annoset.py (пример из Makefile):

    python3 /opt/movidius/ssd-caffe/scripts/create_annoset.py --anno-type=detection \
    --label-map-file=$(wider_dir)/labelmap.prototxt --min-dim=0 --max-dim=0 \
    --resize-width=0 --resize-height=0 --check-label --encode-type=jpg --encoded \
    --redo $(wider_dir) \
    $(wider_dir)/trainval.txt $(wider_dir)/WIDER_train/lmdb/wider_train_lmdb ./data
    

    labelmap.prototxt
    item {
      name: "none_of_the_above"
      label: 0
      display_name: "background"
    }
    item {
      name: "face"
      label: 1
      display_name: "face"
    }
    



    Пример .xml разметки
    <?xml version="1.0" ?>
    <annotation>
    	<size>
    		<width>348</width>
    		<height>450</height>
    		<depth>3</depth>
    	</size>
    	<object>
    		<name>face</name>
    		<bndbox>
    			<xmin>161</xmin>
    			<ymin>43</ymin>
    			<xmax>241</xmax>
    			<ymax>123</ymax>
    		</bndbox>
    	</object>
    </annotation>
    


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

    После этого можно приступать к обучению.

    Обучение


    Код для обучения модели можно найти в моем Colab Notebook.

    Обучение я производил в Google Colaboratory, поскольку мой ноутбук едва справлялся с тестированием сетки, а на обучении вообще зависал. Colaboratory позволила мне обучить сетку достаточно быстро и бесплатно. Подвох лишь в том, что мне пришлось написать скрипт компиляции SSD-Caffe для Colaboratory (включающий такие странные вещи, как перекомпиляцию boost и правку исходников), который выполняется порядка 40 минут. Подробнее можно узнать в моей предыдущей публикации.

    Есть у Colaboratory еще одна особенность: после 12 часов машина умирает, безвозвратно стирая все данные. Лучший способ избежать потери данных — это монтировать в систему свой гугл диск и сохранять в него веса сети каждые 500-1000 итераций обучения.

    Что касается моего детектора, за одну сессию в Colaboratory он успевал отучиться 4500 итераций, и полностью обучался за две сессии.

    Качество предсказаний (mean average precision) на выделенном мной тестовом наборе данных (слитые WIDER и FDDB с ограничениями, перечисленными ранее) составило порядка 0.87 для лучшей модели. Для измерения mAP на сохраненных во время обучения весах есть скрипт scripts/plot_map.py.

    Работа детектора на (очень странном) примере из датасета:


    Запуск на NCS


    Демо-программа с обнаружением лиц находится здесь.

    Чтобы скомпилировать нейросеть для Neural Compute Stick, нужен Movidius NCSDK: он содержит утилиты для компиляции и профилирования нейросетей, а также C++ и Python API. Стоит заметить, что недавно была выпущена вторая версия, не совместимая с первой: все функции API были зачем-то переименованы, поменялся внутренний формат нейросеток, также добавились FIFO для взаимодействия с NCS и (наконец-то) появилось автоматическое преобразование из float 32 bit в float 16 bit, чего так не хватало в C++. Все свои проекты я обновил до второй версии, но оставил пару костылей для совместимости с первой.

    После обучения детектора стоит слить BatchNorm слои с соседними свертками для ускорения нейросети. Этим занимается скрипт merge_bn.py отсюда, который я тоже позаимствовал из проекта Mobilenet-SSD.

    Затем необходимо вызвать утилиту mvNCCompile, например:

    mvNCCompile -s 12 -o graph_ssd -w ssd-face.caffemodel ssd-face.prototxt
    

    В Makefile проекта для этого есть цель graph_ssd. Полученный файл graph_ssd является описанием нейросети в формате, понятном NCS.

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

    • Получить дескриптор устройства по порядковому номеру
    • Открыть устройство
    • Считать скомпилированный файл нейросети в буфер (как бинарный файл)
    • Создать пустой граф вычислений для NCS
    • Разместить граф на устройстве, используя данные из файла, и выделить для него FIFO на input/output; буфер с содержанием файла теперь можно освободить
    • Запуск детектора:
        
      • Получить изображение с камеры (или из любого другого источника)
      • Обработать его: отмасштабировать до нужного размера, преобразовать в float32 и привести к диапазону [-1,1]
      • Загрузить изображение на устройство и запросить инференс
      • Запросить результат (программа заблокируется до момента получения результата)
      • Распарсить результат, выделить рамки объектов (о формате — далее)
      • Вывести изображение с предсказаниями

        
    • Освободить все ресурсы: удалить FIFO и граф вычислений, закрыть устройство и удалить его дескриптор

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

    Создаем обертку, передавая в конструктор размер входа и размер выхода (число элементов):

    NCSWrapper NCS(NETWORK_INPUT_SIZE*NETWORK_INPUT_SIZE*3, NETWORK_OUTPUT_SIZE);
    

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

    if (!NCS.load_file("./models/face/graph_ssd"))
    {
        NCS.print_error_code();
        return 0;
    }
    

    Преобразуем изображение в float32 (image — это cv::Mat в формате CV_32FC3) и загружаем на устройство:

    if(!NCS.load_tensor_nowait((float*)image.data))
    {
        NCS.print_error_code();
        break;
    }
    

    Получаем результат (result — это свободный float указатель, буфер результата поддерживается оберткой); до окончания вычислений программа блокируется:

    if(!NCS.get_result(result))
    {
        NCS.print_error_code();
        break;
    }
    

    На самом деле, в обертке есть и метод, который позволяет загрузить данные и получить результат одновременно: load_tensor((float*)image.data, result). Я отказался от его использования не просто так: используя отдельные методы, можно слегка ускорить выполнение кода. После загрузки изображения CPU будет простаивать до тех пор, пока не придет результат выполнения с NCS (в данном случае это порядка 100 мс), и в это время можно заняться полезной работой: считать новый кадр и преобразовать его, а также вывести на экран предыдущие детекции. Именно так и реализована демо-программа, в моем случае это немного увеличивает FPS. Можно пойти дальше и запускать обработку изображений и детектор лиц асинхронно в двух разных потоках — это действительно работает и позволяет еще немного ускориться, однако в демо-программе не реализовано.

    Детектор в качестве результата возвращает float массив размера 7*(keep_top_k+1). Здесь keep_top_k — параметр, заданный в .prototxt файле модели и показывающий, сколько детекций (в порядке уменьшения уверенности) нужно вернуть. Этот параметр, а также параметр, отвечающий за фильтрацию детекций по минимальному значению уверенности, и параметры non maximum suppression можно настроить в .prototxt файле модели в самом последнем слое. Стоит заметить, что если Caffe возвращает столько детекций, сколько на изображении было найдено, то NCS всегда возвращает keep_top_k детекций, чтобы размер массива был постоянным.

    Сам массив результата устроен так: если рассматривать его как матрицу с keep_top_k+1 строками и 7 столбцами, то в первой строке, в первом элементе будет число детекций, а начиная со второй строки будут сами детекции в формате "garbage, class_index, class_probability, x_min, y_min, x_max, y_max". Координаты указаны в диапазоне [0,1], поэтому их необходимо будет домножить на высоту/ширину изображения. В остальных элементах массива будет мусор. При этом non maximum suppression выполняется автоматически, еще до получения результата (похоже, прямо на NCS).

    Парсинг выхода детектора
    void get_detection_boxes(float* predictions, int w, int h, float thresh, 
    	std::vector<float>& probs, std::vector<cv::Rect>& boxes)
    {
        int num = predictions[0];
        float score = 0;
        float cls = 0;
        
        for (int i=1; i<num+1; i++)
        {
          score = predictions[i*7+2];
          cls = predictions[i*7+1];
          if (score>thresh && cls<=1)
          {
    	probs.push_back(score);
    	boxes.push_back(Rect(predictions[i*7+3]*w, predictions[i*7+4]*h,
    			    (predictions[i*7+5]-predictions[i*7+3])*w, 
    			    (predictions[i*7+6]-predictions[i*7+4])*h));
          }
        }
    }
    


    Особенности запуска на Raspberry Pi


    Сама демо-программа может быть запущена как на обычном компьютере или ноутбуке с Ubuntu, так и на Raspberry Pi с Raspbian Stretch. Я использую Raspberry Pi 2 model B, но демо должно работать и на других моделях. Makefile проекта содержит две цели для переключения режима: make switch_desk для компьютера/ноутбука и make switch_rpi для Raspberry Pi. Принципиальная разница в коде программы заключается лишь в том, что в первом случае для чтения данных с камеры используется OpenCV, а во втором случае — библиотека RaspiCam. Для запуска демо на Raspberry необходимо скомпилировать и установить ее.

    Теперь очень важный момент: установка NCSDK. Если следовать стандартным инструкциям установки на Raspberry Pi, ничем хорошим это не кончится: установщик попытается подтащить и скомпилировать SSD-Caffe и Tensorflow. Вместо этого NCSDK нужно скомпилировать в API-only режиме. В этом режиме будут доступны только C++ и Python API (то есть, невозможно будет компилировать и профилировать графы нейросетей). Это значит, что граф нейросети нужно сначала скомпилировать на обычном компьютере, а затем скопировать на Raspberry. Для удобства я добавил в репозиторию два скомпилированных файла, для YOLO и для SSD.

    Еще один интересный момент — это чисто физическое подключение NCS к Raspberry. Казалось бы, несложно подключить ее к USB-разъему, но нужно помнить, что ее корпус при этом заблокирует остальные три разъема (он довольно здоровый, так как выполняет функцию радиатора). Самый простой выход — подключить ее через USB-кабель.

    Также стоит иметь в виду, что скорость выполнения будет различаться для разных версий USB (для конкретно этой нейросетки: 102 ms для USB 3.0, 92 ms для USB 2.0).

    Теперь насчет питания NCS. Согласно документации, потребляет она до 1 ватта (при 5 вольтах на USB разъеме это будет до 200 ma; для сравнения: камера Raspberry потребляет до 250 ma). При питании от обычного зарядного устройства на 5 вольт, 2 ампера все прекрасно работает. Однако при попытке подключить две или больше NCS к Raspberry могут возникнуть проблемы. В этом случае рекомендуют использовать USB-разветвитель с возможностью внешнего питания.

    На Raspberry демо работает медленнее, чем на компьютере/ноутбуке: 7.2 FPS против 10.4 FPS. Связано это с несколькими факторами: во-первых, от вычислений на CPU избавиться невозможно, а выполняются они намного медленнее; во-вторых, сказывается скорость передачи данных (для USB 2.0).

    Также для сравнения я попытался запустить на Raspberry YOLOv2 детектор лиц из своей первой статьи, но заработал он очень плохо: при скорости в 3.6 FPS он пропускает множество лиц даже на простых кадрах. Судя по всему, он очень чувствителен к параметрам входного изображения, качество которого в случае камеры Raspberry далеко от идеала. SSD работает намного стабильнее, хотя пришлось немного подкрутить параметры видео в настройках RapiCam. он тоже иногда пропускает лица на кадре, но делает это довольно редко. Для увеличения стабильности в реальных приложениях можно добавить простой centroid tracker.

    К слову: то же самое можно воспроизвести и на Python, есть туториал на PyImageSearch (используется Mobilenet-SSD для задачи object detection).

    Другие идеи


    Также я испытал пару идей по ускорению самой нейросети:

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

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

    Заключение


    Об обнаружении лиц на Raspberry я задумался довольно давно, как о подзадаче моего робототехнического проекта. Мне не понравились классические детекторы по соотношению скорости и качества, и я решил попробовать нейросетевые методы, заодно испытав Neural Compute Stick, в результате чего появились два проекта на GitHub и три статьи на Хабре (включая текущую). В целом, результат меня устраивает — скорее всего, именно этот детектор я буду использовать в своем роботе (возможно, о нем будет еще одна статья). Стоит заметить, что мое решение может оказаться не оптимальным — все же, это учебный проект, выполненный отчасти из любопытства к NCS. Все же, надеюсь, что эта статья окажется кому-нибудь полезной.
    Поделиться публикацией
    Комментарии 10
      0
      Отличная статья и проект! Скажите, а Вы не сравнивали по другим источникам скорость работы этой же сетки на GPU? В частности интересно, какую необходимо иметь графическую карту, чтобы добиться тех же 10.4 FPS?
        0
        Я сейчас измерил время исполнения и получил вот что:
        • Core i3 (мой ноут): 281 ms
        • Google Colaboratory (с видеокартой Tesla K80): 71.5 ms
        • NCS (измерено mvNCProfile): 80 ms
        • NCS (реальное исполнение, USB 3.0): 92.4 ms
        • NCS (реальное исполнение, USB 2.0): 102 ms
          +1
          Спасибо за измерения! Получается, что NCS соизмерим по скорости с GPU.
        0
        «Но стоит иметь в виду, что кабель вносит дополнительную задержку при передаче данных — не очень большую, но заметную. Я пробовал два разных кабеля, один 2 м, второй 30 см, и они оба вносили примерно одинаковую задержку.»
        А можно чуть подробнее? какая была задержка при прямом втыкании и через кабель. Я как-то не думал, что USB настолько быстр, что скорость эм.волны в проводнике начинает играть роль.
          +1
          Не специалист, но подозреваю что дело в большей чувствительности кабеля к наводкам — интерфейс либо сбрасывает скорость, либо повторно передаёт битые фрагменты. Возможно, экранированный кабель не давал бы такого эффекта?
            0
            Так, это мой косяк. На самом деле проблема не в кабеле, а в версии USB (если на USB 3.0 фактическое время исполнения составляет 92.4 ms, то на USB 2.0 — 102 ms, независимо от наличия кабеля). Видимо, оба кабеля не поддерживают USB 3.0. Сейчас поправлю в статье.
              0
              Кабеля не поддерживают usb 3.0? 8\
                0
                Мда, кривовато высказался. Но смысл-то ясен, дело в версии USB .-.
            0
            В интернете мелькала история как один гик в США снабдил свой дом умной системой распознавания лиц с последующим автоматическим открыванием двери родного дома при детектировании лица хозяина. Однажды он пришел домой, а система распознала лицо Бэтмена в маске нарисованное на его футболке за несанкционированного посетителя и заблокировала дверь.
            В комментариях русские пользователи начали мечтательно размышлять как было бы хорошо, если бы такой умный домик такого хитрого братца-гика находился за МКАД, желательно где-то рядом с Кемерово или Магаданом, при несомненной доступности фото богатого братца-гика в социальных сетях.
            Так это я к чему?
            Распознаванием лица уже в принципе никого не удивишь.
            Что насчет распознавания и сортировки напечатанного лица от настоящего?
              0
              Ну, начнем с того, что обнаружение лиц, распознавание лиц и обнаружение спуфинг-атак — это три разные задачи, которые решаются по-разному.

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

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

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

            Самое читаемое