Распознавание лиц на видеопотоке
В этом туториале Вы узнаете, как распознавать лица на видеопотоке. Для распознавания будет использоваться готовая база лиц из дистрибутива Face SDK (в базе находятся фотографии нескольких знаменитых людей). Распознанные на видеопотоке лица будут подсвечиваться зеленым прямоугольником, рядом с лицом будет отображаться фотография соответствующего человека из базы лиц и его имя. В основе данного туториала лежит туториал Детекция и трекинг лиц на видеопотоке и соответствующий проект, данный туториал является его продолжением и дополнением.
Готовый демо-проект вы можете найти в дистрибутиве Face SDK: examples/tutorials/face_recognition_with_video_worker
Подготовка проекта
- Поскольку в туториале Детекция и трекинг лиц на видеопотоке мы указывали только два параметра Face SDK (путь до дистрибутива Face SDK и имя конфигурационного файла для объекта
VideoWorker
), а в данном проекте их будет несколько (добавляется путь до базы лиц, имя конфигурационного файла с методом распознавания, FAR), для удобства мы модифицируем несколько файлов. Укажем все параметры в структуреFaceSdkParameters
. В заголовочном файлеfacesdkparameters.h
указываем путь до конфигурационного файлаvideo_worker_lbf.xml
.
facesdkparameters.h
struct FaceSdkParameters
{
...
std::string videoworker_config = "video_worker_lbf.xml";
};
- Передадим структуру
face_sdk_parameters
в конструктор объектаWorker
.
viewwindow.h
class ViewWindow : public QWidget
{
Q_OBJECT
public:
explicit ViewWindow(
QWidget *parent,
pbio::FacerecService::Ptr service,
FaceSdkParameters face_sdk_parameters);
...
private:
...
std::shared_ptr<Worker> _worker;
pbio::FacerecService::Ptr _service;
};
viewwindow.cpp
ViewWindow::ViewWindow(
QWidget *parent,
pbio::FacerecService::Ptr service,
FaceSdkParameters face_sdk_parameters) :
QWidget(parent),
ui(new Ui::ViewWindow),
_service(service)
{
ui->setupUi(this);
...
_worker = std::make_shared<Worker>(
_service,
face_sdk_parameters);
...
};
worker.h
#include "qcameracapture.h"
#include "facesdkparameters.h"
...
class Worker
{
...
Worker(
const pbio::FacerecService::Ptr service,
const FaceSdkParameters face_sdk_parameters);
...
};
worker.cpp
Worker::Worker(
const pbio::FacerecService::Ptr service,
const FaceSdkParameters face_sdk_parameters)
{
pbio::FacerecService::Config vwconfig(face_sdk_parameters.videoworker_config);
...
}
- В данном проекте нас интересует исключительно детекция лица на видеопотоке (создание ограничивающего прямоугольника) и последующее распознавание. Обратите внимание, что в первом проекте (detection_and_tracking_with_video_worker), который Вы можете использовать для создания данного проекта, помимо ограничивающего прямоугольника также отображаются антропометрические точки и углы. При необходимости Вы можете удалить ненужную визуализацию из первого проекта.
Создание базы лиц
- Прежде всего нам нужно создать базу лиц. Чтобы проверить распознавание лиц, вы можете использовать готовую базу из дистрибутива Face SDK. Она включает в себя фотографии трех знаменитостей (Илон Маск, Эмилия Кларк, Лионель Месси). Для проверки распознавания вы можете скопировать готовую базу в корневую папку проекта (рядом с .pro файлом), запустить проект, открыть фотографию из базы и направить камеру на монитор. Также вы можете добавить в базу свою фотографию для распознавания. Для этого вам нужно создать папку в базе, указать ваше имя в названии и поместить в эту папку ваше фото (по аналогии с другими папками).
- Создаем новый класс
Database
для работы с базой лиц: Add New > C++ > C++ Class > Choose… > Class name – Database > Next > Project Management (настройки по умолчанию) > Finish. В заголовочном файлеdatabase.h
подключаем заголовочные файлыQImage
,QString
для работы с изображениями и строками иlibfacerec.h
для интеграции Face SDK.
database.h
#include <QImage>
#include <QString>
#include <facerec/libfacerec.h>
class Database
{
public:
Database();
}
- В файле
database.cpp
подключаем заголовочные файлыdatabase.h
иvideoframe.h
(реализация интерфейсаIRawImage
, через которыйVideoWorker
принимает кадры). Также добавляем необходимые заголовочные файлы для работы с файловой системой, отладки, обработки исключений и работы с файлами.
database.cpp
#include "database.h"
#include "videoframe.h"
#include <QDir>
#include <QDebug>
#include <stdexcept>
#include <fstream>
- В заголовочном файле
database.h
добавляем конструктор, где прописываем путь до базы лиц, а также указываем объектRecognizer
для создания шаблонов, объектCapturer
для детекции лиц и far (вероятность того, что один человек может быть принят за другого). Векторvw_elements
содержит в себе элементы базы данныхVideoWorker
. Векторыthumbnails
иnames
содержат в себе превью фотографий и имена лиц из базы.
database.h
class Database
{
public:
...
// Создаем базу
Database(
const std::string database_dir_path,
pbio::Recognizer::Ptr recognizer,
pbio::Capturer::Ptr capturer,
const float fa_r);
std::vector<pbio::VideoWorker::DatabaseElement> vw_elements;
std::vector<QImage> thumbnails;
std::vector<QString> names;
}
- В файле
database.cpp
добавляем реализацию конструктораDatabase
, объявленного в предыдущем пункте. Величинаdistance_threshold
означает порог распознавания. Поскольку для разных методов этот порог разный, мы получаем его из значения FAR с помощью методаgetROCCurvePointByFAR
.
database.cpp
Database::Database(
const std::string database_dir_path,
pbio::Recognizer::Ptr recognizer,
pbio::Capturer::Ptr capturer,
const float fa_r)
{
const float distance_threshold = recognizer->getROCCurvePointByFAR(fa_r).distance;
}
- В переменной
database_dir
указываем путь до директории с базой лиц. Если путь не существует, то будет выводиться ошибка “database directory doesn’t exist”. Создаем переменнуюperson_id
для хранения id человека из базы (соотносится с папкой “person...”) и переменнуюelement_id_counter
для хранения id элемента в базе (соотносится с фотографией человека из базы). В спискеdirs
формируем список всех поддиректорий из указанной директории с базой.
database.cpp
Database::Database(
const std::string database_dir_path,
pbio::Recognizer::Ptr recognizer,
pbio::Capturer::Ptr capturer,
const float fa_r)
{
...
QDir database_dir(QString::fromStdString(database_dir_path));
if (!database_dir.exists())
{
throw std::runtime_error(database_dir_path + ": database directory doesn't exist");
}
int person_id = 0;
int element_id_counter = 0;
QFileInfoList dirs = database_dir.entryInfoList(
QDir::AllDirs | QDir::NoDotAndDotDot,
QDir::DirsFirst);
}
Примечание: Более подробную информацию о значениях FAR и TAR для разных методов распознавания см. в пункте Характеристики идентификации.
- В цикле
for(const auto &dir: dirs)
обрабатываем каждую поддиректорию – данные для отдельного человека. В качестве имени человека берем название директории. Формируем список изображений вperson_files
.
database.cpp
Database::Database(...)
{
...
for(const auto &dir: dirs)
{
QDir person_dir(dir.filePath());
QString name = dir.baseName();
QFileInfoList person_files = person_dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
}
}
- Во вложенном цикле
for(const auto &person_file: person_files)
обрабатываем каждое изображение. В случае, если фото не существует, выводится предупреждение “Can’t read image”.
database.cpp
Database::Database(...)
{
...
for(const auto &dir: dirs)
{
...
QFileInfoList person_files = person_dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
for(const auto &person_file: person_files)
{
QString path = person_file.filePath();
qDebug() << "processing" << path << "name:" << name;
QImage image(path);
if(image.isNull())
{
qDebug() << "\n\nWARNING: cant read image" << path << "\n\n";
continue;
}
if (image.format() != QImage::Format_RGB888)
{
image = image.convertToFormat(QImage::Format_RGB888);
}
VideoFrame frame;
frame.frame() = QCameraCapture::FramePtr(new QImage(image));
}
}
}
- Детектируем лицо на фото, используя объект
Capturer
. В случае, если фото невозможно прочитать, на фото не удалось найти лицо или найдено более одного лица, выводится предупреждение и фото игнорируется.
database.cpp
Database::Database(...)
{
...
for(const auto &dir: dirs)
{
...
QFileInfoList person_files = person_dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
for(const auto &person_file: person_files)
{
...
// Детектируем лица
const std::vector<pbio::RawSample::Ptr> captured_samples = capturer->capture(frame);
if(captured_samples.size() != 1)
{
qDebug() << "\n\nWARNING: detected" << captured_samples.size() <<
"faces on" << path << "image instead of one, image ignored\n\n";
continue;
}
const pbio::RawSample::Ptr sample = captured_samples[0];
}
}
}
- При помощи метода
recognizer->processing
создаем шаблон лица для распознавания.
database.cpp
Database::Database(...)
{
...
for(const auto &dir: dirs)
{
...
QFileInfoList person_files = person_dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
for(const auto &person_file: person_files)
{
...
// Создаем шаблон
const pbio::Template::Ptr templ = recognizer->processing(*sample);
}
}
}
- В структуре
pbio::VideoWorker::DatabaseElement vw_element
указываем всю информацию об элементе базы, которая в дальнейшем будет передана для обработки объектуVideoWorker
(уникальный идентификатор элемента, уникальный идентификатор человека, шаблон лица, порог распознавания). Используя методpush_back
, добавляем элемент в конец списка.
database.cpp
Database::Database(...)
{
...
for(const auto &dir: dirs)
{
...
QFileInfoList person_files = person_dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
for(const auto &person_file: person_files)
{
...
// Подготавливаем данные для VideoWorker
pbio::VideoWorker::DatabaseElement vw_element;
vw_element.element_id = element_id_counter++;
vw_element.person_id = person_id;
vw_element.face_template = templ;
vw_element.distance_threshold = distance_threshold;
vw_elements.push_back(vw_element);
thumbnails.push_back(makeThumbnail(image));
names.push_back(name);
}
++person_id;
}
}
- В заголовочном файле
database.h
добавляем методmakeThumbnail
для создания превью фотографии лица из базы.
database.cpp
class Database
{
public:
// Создаем превью из сэмпла
static
QImage makeThumbnail(const QImage& image);
...
};
- В файле
database.cpp
добавляем реализацию метода, используя методmakeThumbnail
для создания превью фотографии из базы лиц, которое будет отображаться рядом с распознанным лицом. Задаем его размер (120 пикселей) и масштабируем (при уменьшении пропорции изображения будут сохраняться).
database.cpp
#include <fstream>
...
QImage Database::makeThumbnail(const QImage& image)
{
const float thumbnail_max_side_size = 120.f;
const float scale = thumbnail_max_side_size / std::max<int>(image.width(), image.height());
QImage result = image.scaled(
image.width() * scale,
image.height() * scale,
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
return result;
}
- В pro-файле зададим путь до базы лиц.
face_recognition_with_video_worker.pro
...
DEFINES += FACE_SDK_PATH=\\\"$$FACE_SDK_PATH\\\"
DEFINES += DATABASE_PATH=\\\"$${_PRO_FILE_PWD_}/base\\\"
INCLUDEPATH += $${FACE_SDK_PATH}/include
...
- В заголовочном файле facesdkparameters.h указываем путь до базы лиц, а также значение FAR.
facesdkparameters.h
struct FaceSdkParameters
{
...
std::string videoworker_config = "video_worker_lbf.xml";
std::string database_dir = DATABASE_PATH;
const float fa_r = 1e-5;
};
Поиск лица по базе и отображение результата в консоли
- В заголовочном файле
facesdkparameters.h
указываем путь до конфигурационного файла с методом распознавания. В данном проекте мы используем метод 6.7, потому что он подходит для обработки видеопотока и обеспечивает оптимальную скорость распознавания и хорошее качество. Более подробную информацию о рекомендуемых методах распознавания см. в пункте Идентификация лиц.
facesdkparameters.h
struct FaceSdkParameters
{
...
std::string videoworker_config = "video_worker_lbf.xml";
std::string database_dir = DATABASE_PATH;
const float fa_r = 1e-5;
std::string method_config = "method6v7_recognizer.xml";
};
Примечание: Если вы хотите распознавать лица на видеопотоке на низкопроизводительных устройствах, вы можете использовать метод 9.30. При этом скорость распознавания будет выше, чем с методом 6.7, но качество распознавания будет ниже.
- В заголовочном файле
worker.h
в структуреFaceData
добавляем переменнуюmatch_database_index
, в которой в случае совпадения лица с человеком из базы будет записан номер элемента из базы или"-1"
в случае несовпадения. Также подключаем базу лицDatabase
и добавляем коллбэк распознаванияMatchFoundCallback
.
worker.h
#include "qcameracapture.h"
#include "facesdkparameters.h"
#include "database.h"
...
class Worker
{
public:
struct FaceData
{
pbio::RawSample::Ptr sample;
bool lost;
int frame_id;
int match_database_index;
FaceData() :
lost(true),
match_database_index(-1)
{
}
};
...
pbio::VideoWorker::Ptr _video_worker;
Database _database;
...
static void TrackingLostCallback(
const pbio::VideoWorker::TrackingLostCallbackData &data,
void* const userdata);
static void MatchFoundCallback(
const pbio::VideoWorker::MatchFoundCallbackData &data,
void* const userdata);
int _tracking_callback_id;
int _tracking_lost_callback_id;
int _match_found_callback_id;
};
- В файле
worker.cpp
переопределяем значение параметра в конфигурационном файле, чтобы коллбэкMatchFoundCallback
приходил в том числе и для нераспознанных лиц. Укажем параметры объектаVideoWorker
: в первом туториале распознавание лиц не производилось, поэтому мы указывали только значениеstreams_count
. Поскольку в этом проекте мы распознаем лица на видеопотоке, нам нужно указать в конструкторе путь до конфигурационного файла с методом распознавания, а также значенияprocessing_threads_count
(количество потоков для создания шаблонов) иmatching_threads_count
(количество потоков для сравнения шаблонов). В данном проекте используется один видеопоток (подключенная камера). Подключаем базу лиц: передаем путь до базы, создаем объектCapturer
для детекции лиц и объектRecognizer
для построения шаблонов, а также указываем коэффициент FAR. Используя методsetDatabase
, задаем базу лиц для объектаVideoWorker
. Используя методaddMatchFoundCallback
, добавляем обработчик события распознаванияMatchFound
.
worker.cpp
Worker::Worker(
const pbio::FacerecService::Ptr service,
const FaceSdkParameters face_sdk_parameters)
{
pbio::FacerecService::Config vwconfig(face_sdk_parameters.videoworker_config);
vwconfig.overrideParameter("not_found_match_found_callback", 1);
_video_worker = service->createVideoWorker(
vwconfig,
face_sdk_parameters.method_config,
1, // streams_count
1, // processing_threads_count
1); // matching_threads_count
_database = Database(
face_sdk_parameters.database_dir,
service->createRecognizer(face_sdk_parameters.method_config, true, false),
service->createCapturer("common_capturer4_lbf_singleface.xml"),
face_sdk_parameters.fa_r);
_video_worker->setDatabase(_database.vw_elements);
...
_match_found_callback_id =
_video_worker->addMatchFoundCallbackU(
MatchFoundCallback,
this);
}
- В деструкторе
Worker::~Worker()
удаляем коллбэкMatchFoundCallback
.
worker.cpp
Worker::~Worker()
{
_video_worker->removeTrackingCallback(_tracking_callback_id);
_video_worker->removeTrackingLostCallback(_tracking_lost_callback_id);
_video_worker->removeMatchFoundCallback(_match_found_callback_id);
}
...
- В коллбэке
MatchFoundCallback
результат приходит в виде структурыMatchFoundCallbackData
, которая хранит данные о распознанных и нераспознанных лицах.
worker.cpp
// static
void Worker::TrackingLostCallback(
const pbio::VideoWorker::TrackingLostCallbackData &data,
void* const userdata)
{
...
}
// static
void Worker::MatchFoundCallback(
const pbio::VideoWorker::MatchFoundCallbackData &data,
void* const userdata)
{
assert(userdata);
const pbio::RawSample::Ptr &sample = data.sample;
const pbio::Template::Ptr &templ = data.templ;
const std::vector<pbio::VideoWorker::SearchResult> &search_results = data.search_results;
// Информация о пользователе - указатель на Worker
// Передаем указатель
Worker &worker = *reinterpret_cast<Worker*>(userdata);
assert(sample);
assert(templ);
assert(!search_results.empty());
}
- Когда для отслеживаемого лица создается шаблон, он сравнивается с каждым шаблоном из базы, и если расстояние до ближайшего элемента оказывается меньше порога
distance_threshold
, указанного в этом элементе, то фиксируется совпадение. В случае, если лицо на видеопотоке не распознано, выводится сообщение “Match not found”. Если лицо распознано, то в консоли выводится сообщение “Match found with...” и имя человека, с которым найдено соответствие.
worker.cpp
// static
void Worker::MatchFoundCallback(
const pbio::VideoWorker::MatchFoundCallbackData &data,
void* const userdata)
{
...
for(const auto &search_result: search_results)
{
const uint64_t element_id = search_result.element_id;
if(element_id == pbio::VideoWorker::MATCH_NOT_FOUND_ID)
{
std::cout << "Match not found" << std::endl;
}
else
{
assert(element_id < worker._database.names.size());
std::cout << "Match found with '"
<< worker._database.names[element_id].toStdString() << "'";
}
}
std::cout << std::endl;
}
- Сохраняем данные о распознанном лице для отрисовки превью.
worker.cpp
// static
void Worker::MatchFoundCallback(
const pbio::VideoWorker::MatchFoundCallbackData &data,
void* const userdata)
{
...
const uint64_t element_id = search_results[0].element_id;
if(element_id != pbio::VideoWorker::MATCH_NOT_FOUND_ID)
{
assert(element_id < worker._database.thumbnails.size());
// Сохраняем результат лучшего соответствия для последующей отрисовки
const std::lock_guard<std::mutex> guard(worker._drawing_data_mutex);
FaceData &face = worker._drawing_data.faces[sample->getID()];
assert(!face.lost);
face.match_database_index = element_id;
}
}
- Запускаем проект. В консоли будут отображаться результаты распознавания. Если лицо распознано, будет отображаться id лица и имя распознанного человека из базы. Если лицо не распознано, будет отображаться сообщение “Match not found”.
Отображение превью распознанного лица из базы
- Для удобства восприятия добавим отображение фото и имени человека из базы рядом с лицом на видеопотоке. В файле
drawfunction.h
добавляем ссылку на базу лиц, поскольку она потребуется при отрисовке результатов распознавания.
drawfunction.h
#include "database.h"
class DrawFunction
{
public:
DrawFunction();
static QImage Draw(
const Worker::DrawingData &data,
const Database &database);
};
- В файле
drawfunction.cpp
модифицируем функциюDrawFunction::Draw
, передав в нее базу лиц.
drawfunction.cpp
// static
QImage DrawFunction::Draw(
const Worker::DrawingData &data,
const Database &database)
{
...
const pbio::RawSample& sample = *face.sample;
QPen pen;
}
- Сохраняем ограничивающий прямоугольник лица в структуре
pbio::RawSample::Rectangle
. Передаем его параметры (x, y, ширина, высота) объектуQRect rect
.
drawfunction.cpp
QImage DrawFunction::Draw(...)
{
...
// Сохраняем ограничивающий прямоугольник лица
const pbio::RawSample::Rectangle bounding_box = sample.getRectangle();
QRect rect(bounding_box.x, bounding_box.y, bounding_box.width, bounding_box.height);
}
- Создаем булеву переменную
recognized
, которая будет обозначать, распознано лицо или не распознано. Если лицо распознано, ограничивающий прямоугольник будет зеленого цвета, если нет – красного.
drawfunction.cpp
QImage DrawFunction::Draw(...)
{
...
const bool recognized = face.match_database_index >= 0;
const QColor color = recognized ?
Qt::green :
Qt::red; // Красным выделяются нераспознанные лица
// Отображаем ограничивающий прямоугольник лица
{
pen.setWidth(3);
pen.setColor(color);
painter.setPen(pen);
painter.drawRect(rect);
}
}
- По номеру
face.match_database_index
получаем изображение из базы для превью. Рассчитываем положение превью в кадре.
drawfunction.cpp
QImage DrawFunction::Draw(...)
{
...
// Отображаем фото из базы
if (recognized)
{
const QImage thumbnail = database.thumbnails[face.match_database_index];
// Рассчитываем положение превью
QPoint preview_pos(
rect.x() + rect.width() + pen.width(),
rect.top());
}
- Рисуем на превью изображение из базы. Создаем объект
QImage face_preview
, который больше по высоте, чемthumbnail
наtext_bar_height
. В позиции (0, 0) рисуется исходное изображение превью. В результате мы получаем превью с черным прямоугольником в нижней части, в котором выводится имя человека. Зададим параметры шрифта, рассчитаем положение надписи и отобразим текст на превью.
drawfunction.cpp
QImage DrawFunction::Draw(...)
{
...
// Отображаем фото из базы
if (recognized)
{
...
const int text_bar_height = 20;
QImage face_preview(
QSize(thumbnail.width(), thumbnail.height() + text_bar_height),
QImage::Format_RGB888);
face_preview.fill(Qt::black);
{
const int font_size = 14;
QPainter painter_preview(&face_preview);
painter_preview.drawImage(QPoint(0, 0), thumbnail);
painter_preview.setFont(QFont("Arial", font_size, QFont::Medium));
pen.setColor(Qt::white);
painter_preview.setPen(pen);
painter_preview.drawText(
QPoint(0, thumbnail.height() + text_bar_height - (text_bar_height - font_size) / 2),
database.names[face.match_database_index]);
}
}
}
- Отрисовываем превью
face_preview
на кадре, используя методdrawPixmap
.
drawfunction.cpp
// static
QImage DrawFunction::Draw(...)
{
...
// Отображаем фото из базы
if (recognized)
{
...
QPixmap pixmap;
pixmap.convertFromImage(face_preview);
painter.drawPixmap(preview_pos, pixmap);
}
}
- В файл
worker.h
добавляем метод, возвращающий ссылку на базу лиц.
worker.h
class Worker
{
public:
...
void getDataToDraw(DrawingData& data);
const Database& getDatabase() const
{
return _database;
}
};
- Модифицируем вызов функции
DrawFunction::Draw
, передав в нее базу лиц.
viewwindow.cpp
void ViewWindow::draw()
{
...
const QImage image = DrawFunction::Draw(data, _worker->getDatabase());
ui->frame->setPixmap(QPixmap::fromImage(image));
}
- Запускаем проект. Если лицо распознано, оно будет выделяться зеленым прямоугольником, и справа от него будет отображаться уменьшенная копия фото из базы и имя. Нераспознанные лица будут выделяться красным прямоугольником.