Перевод статьи Node.js + face-recognition.js: Simple and Robust Face Recognition using Deep Learning.
В этой статье мы расскажем, как реализовать надёжную систему распознавания лиц с использованием face-recognition.js. Мы искали подходящую Node.js-библиотеку, которая умела бы аккуратно распознавать лица, но ничего не нашли. Пришлось писать самостоятельно!
В этом npm-пакете используется библиотека dlib, предоставляющая Node.js-биндинги для очень хорошо зарекомендовавших себя инструментов распознавания внутри этой библиотеки. Dlib использует методы глубокого обучения и поставляется с уже обученными моделями, которые продемонстрировали точность распознавания на уровне 99,38% при прогоне бенчмарка LFW.
Зачем?
В последнее время мы пытаемся создать приложение для распознавания лиц на основе Node.js, которое выделяло бы и распознавало лица персонажей из сериала «Теория Большого взрыва». Сначала планировалось сделать приложение с использованием средств распознавания OpenCV, как описано в статье Node.js + OpenCV for Face Recognition.
Однако, несмотря на высокую скорость, качество работы этих инструментов оказалось недостаточной. Точнее, они хорошо справлялись с лицами анфас, но стоило человеку немного отвернуться от камеры, и качество распознавания снижалось.
В поисках решения под руку попалась С++-библиотека dlib; мы повозились с Python API, впечатлились результатом и, наконец, решили: будем использовать эту библиотеку вместе с Node.js! Так на свет появился этот npm-пакет, предоставляющий упрощённый Node.js API для распознавания лиц.
И что такое face-recognition.js?
Хотелось сделать пакет на основе face-recogntion.js, который:
- позволяет быстро начать распознавать лица благодаря простому API;
- при необходимости позволяет выполнять тонкие настройки;
- прост в установке (в идеале, чтобы достаточно было написать
npm install
).
Хотя пакет ещё не закончен, вы уже можете скачать некоторые инструменты.
Face Detection (определение лиц)
Для быстрого и не слишком надёжного определения лиц на изображении вы можете использовать нейросеть глубокого обучения или простой фронтальный распознаватель:
Face Recognizer (распознавание лиц)
Распознаватель — это нейросеть глубокого обучения, использующая для вычисления уникальных дескрипторов лиц упомянутые выше модели. Обучать распознаватель можно на массиве тэгированных изображений лиц, после чего он сможет помечать лица на входном изображении:
Face Landmarks (опорные точки)
С помощью этого пакета вы также можете определять от 5 до 68 опорных точек на лицах:
Отличная история, покажи теперь, как это работает!
Итак, решить задачу с помощью OpenCV не удалось. У нас осталась пачка лиц Шелдона, Раджеша, Леонард, Говарда и Стюарта размером 150 х 150 пикселей каждое. С помощью этих данных можно с лёгкостью научить Face Recognizer распознавать новые лица. Код этого примера лежит в репозитории.
Подготовка данных
Мы собрали примерно по 20 лиц каждого персонажа в разных ракурсах:
Возьмём по 10 лиц для обучения, а остальные используем для оценки точности распознавания:
const path = require('path')
const fs = require('fs')
const fr = require('face-recognition')
const dataPath = path.resolve('./data/faces')
const classNames = ['sheldon', 'lennard', 'raj', 'howard', 'stuart']
const allFiles = fs.readdirSync(dataPath)
const imagesByClass = classNames.map(c =>
allFiles
.filter(f => f.includes(c))
.map(f => path.join(dataPath, f))
.map(fp => fr.loadImage(fp))
)
const numTrainingFaces = 10
const trainDataByClass = imagesByClass.map(imgs => imgs.slice(0, numTrainingFaces))
const testDataByClass = imagesByClass.map(imgs => imgs.slice(numTrainingFaces))
В каждом имени файла указано имя персонажа, так что мы с лёгкостью сопоставим наши имена классов:
['sheldon', 'lennard', 'raj', 'howard', 'stuart']
с массивами изображений по каждому классу. С помощью fr.loadImage(fp)
вы можете считать изображение, указанное в пути к файлу.
Определение лиц
Изображения лиц размером 150 х 150 пикселей были заранее вырезаны с помощью opencv4nodejs. Но вы можете определить лица, вырезать, сохранить и маркировать их таким образом:
const image = fr.loadImage('image.png')
const detector = fr.FaceDetector()
const targetSize = 150
const faceImages = detector.detectFaces(image, targetSize)
faceImages.forEach((img, i) => fr.saveImage(img, `face_${i}.png`))
Обучение распознавателя
Теперь можно приступать к обучению:
const recognizer = fr.FaceRecognizer()
trainDataByClass.forEach((faces, label) => {
const name = classNames[label]
recognizer.addFaces(faces, name)
})
Этот код скармливает лица нейросети, которая выдаёт для каждого лица дескриптор и сохраняет его в соответствующем классе. Задав в качестве третьего аргумента numJitters
, вы можете применять вращение, масштабирование и зеркалирование, создавая разные версии каждого из входных лиц. Увеличение количества модификаций может повысить точность распознавания, но при этом нейросеть учится дольше.
Также вы можете сохранять состояние распознавателя, чтобы не обучать его заново каждый раз, а просто загружать из файла:
Сохранение:
const modelState = recognizer.serialize()
fs.writeFileSync('model.json', JSON.stringify(modelState))
Загрузка:
const modelState = require('model.json')
recognizer.load(modelState)
Распознавание новых лиц
Теперь с помощью контрольных данных проверим точность распознавания и сохраним результаты в лог:
const errors = classNames.map(_ => [])
testDataByClass.forEach((faces, label) => {
const name = classNames[label]
console.log()
console.log('testing %s', name)
faces.forEach((face, i) => {
const prediction = recognizer.predictBest(face)
console.log('%s (%s)', prediction.className, prediction.distance)
// count number of wrong classifications
if (prediction.className !== name) {
errors[label] = errors[label] + 1
}
})
})
// print the result
const result = classNames.map((className, label) => {
const numTestFaces = testDataByClass[label].length
const numCorrect = numTestFaces - errors[label].length
const accuracy = parseInt((numCorrect / numTestFaces) * 10000) / 100
return `${className} ( ${accuracy}% ) : ${numCorrect} of ${numTestFaces} faces have been recognized correctly`
})
console.log('result:')
console.log(result)
Сейчас распознавание выполняется так: сначала у входного лица вычисляется евклидово расстояние дескрипторного вектора до каждого дескриптора класса, а затем вычисляется среднее значение всех расстояний. Вы можете возразить, что для этой задачи лучше подойдёт кластеризация методом k-средних или SVM-классификатор. Возможно, в будущем они тоже будут реализованы, но скорости и эффективности евклидова расстояния пока что вполне достаточно.
При вызове predictBest
мы получим результат с наименьшим евклидовым расстоянием, то есть с наибольшей похожестью. Примерно так:
{ className: 'sheldon', distance: 0.5 }
Если вам нужно для конкретного лица получить расстояния дескрипторов всех классов, то можете просто использовать recognizer.predict(image)
, который для каждого класса выдаст массивы расстояний:
[
{ className: 'sheldon', distance: 0.5 },
{ className: 'raj', distance: 0.8 },
{ className: 'howard', distance: 0.7 },
{ className: 'lennard', distance: 0.69 },
{ className: 'stuart', distance: 0.75 }
]
Результаты
Если выполнить вышеприведённый код, то получим такие результаты.
Обучим на 10 лицах каждого персонажа:
sheldon ( 90.9% ) : 10 of 11 faces have been recognized correctly
lennard ( 100% ) : 12 of 12 faces have been recognized correctly
raj ( 100% ) : 12 of 12 faces have been recognized correctly
howard ( 100% ) : 12 of 12 faces have been recognized correctly
stuart ( 100% ) : 3 of 3 faces have been recognized correctly
Обучим только на 5 лицах каждого персонажа:
sheldon ( 100% ) : 16 of 16 faces have been recognized correctly
lennard ( 88.23% ) : 15 of 17 faces have been recognized correctly
raj ( 100% ) : 17 of 17 faces have been recognized correctly
howard ( 100% ) : 17 of 17 faces have been recognized correctly
stuart ( 87.5% ) : 7 of 8 faces have been recognized correctly
А вот как это выглядит на видео:
Заключение
Судя по результатам, даже небольшая выборка обучающих данных позволяет получить довольно точное распознавание. И это несмотря на то, что некоторые из входных изображений очень размыты из-за маленького размера.
Автор: NIX_Solutions