Детекция и трекинг лиц на видеопотоке
В этом туториале вы узнаете, как осуществлять детекцию и трекинг лиц на видеопотоке с камеры при помощи объекта VideoWorker
из Face SDK API. Трекаемые лица будут выделяться зеленым прямоугольником.
Помимо Face SDK и Qt, для данного проекта Вам потребуется камера, подключенная к ПК (например, веб-камера). Проект можно запустить как на Windows, так и на Ubuntu (версия 16.04 или выше).
Готовый демо-проект вы можете найти в дистрибутиве Face SDK: examples/tutorials/detection_and_tracking_with_video_worker
Создаем проект Qt
- Запускаем Qt и создаем новый проект: File > New File or Project > Application > Qt Widgets Application > Choose...
- Называем проект, например, detection_and_tracking_with_video_worker, и выбираем путь. Кликаем Next, в разделе Kit Selection выбираем платформу для приложения, например, Desktop. Кликаем Details и отмечаем конфигурацию сборки Release (Debug в данном проекте нам не понадобится).
- В окне Class Information оставляем настройки по умолчанию, кликаем Next, в окне Project Management также оставляем настройки по умолчанию и кликаем Finish.
- Дадим заголовок главному окну приложения: в дереве проекта открываем двойным кликом файл Forms > mainwindow.ui. В правой части редактора во вкладке свойств указываем название окна: windowTitle > Face SDK Tracking.
- Для выравнивания виджетов по сетке перенесем на виджет окна MainWindow объект Grid Layout. Вызываем у виджета MainWindow контекстное меню правым кликом и выбираем Layout > Lay Out in a Grid. Объект Grid Layout должен растянуться по размерам виджета MainWindow. Изменим имя Layout > layoutName > viewLayout.
- Запускаем проект, кликнув Run (Ctrl+R). На экране должно появиться пустое окно с заголовком Face SDK Tracking.
Выводим изображение с камеры
- Для того, чтобы мы могли использовать в проекте камеру, нам нужно подключить мультимедийные виджеты Qt. Для этого добавляем в pro-файл проекта строку:
detection_and_tracking_with_video_worker.pro
...
QT += multimedia multimediawidgets
...
- Создадим новый класс
QCameraCapture
для получения изображения с камеры: Add New > C++ > C++ Class > Choose… > Class name – QCameraCapture > Base class – QObject > Next > Project Management (настройки по умолчанию) > Finish. В заголовочном файлеqcameracapture.h
создаем классCameraSurface
, который будет предоставлять кадры с камеры через коллбэк-функциюpresent
.
qcameracapture.h
#include <QCamera>
#include <QAbstractVideoSurface>
#include <QVideoSurfaceFormat>
#include <QVideoFrame>
class CameraSurface : public QAbstractVideoSurface
{
Q_OBJECT
public:
explicit CameraSurface(QObject* parent = 0);
bool present(const QVideoFrame& frame);
QList<QVideoFrame::PixelFormat> supportedPixelFormats(
QAbstractVideoBuffer::HandleType type = QAbstractVideoBuffer::NoHandle) const;
bool start(const QVideoSurfaceFormat& format);
signals:
void frameUpdatedSignal(const QVideoFrame&);
};
- Описываем реализацию класса в файле
qcameracapture.cpp
. Определяем конструкторCameraSurface::CameraSurface
и методsupportedPixelFormats
. В методеCameraSurface::supportedPixelFormats
перечисляем форматы, поддерживаемые Face SDK (RGB24, BGR24, NV12, NV21). Изображение с некоторых камер приходит в формате RGB32, поэтому мы также указываем его в списке. Этот формат не поддерживается Face SDK, поэтому далее мы оформим преобразование изображения формата RGB32 в формат RGB24.
qcameracapture.cpp
#include "qcameracapture.h"
...
CameraSurface::CameraSurface(QObject* parent) :
QAbstractVideoSurface(parent)
{
}
QList<QVideoFrame::PixelFormat> CameraSurface::supportedPixelFormats(
QAbstractVideoBuffer::HandleType handleType) const
{
if (handleType == QAbstractVideoBuffer::NoHandle)
{
return QList<QVideoFrame::PixelFormat>()
<< QVideoFrame::Format_RGB32
<< QVideoFrame::Format_RGB24
<< QVideoFrame::Format_BGR24
<< QVideoFrame::Format_NV12
<< QVideoFrame::Format_NV21;
}
return QList<QVideoFrame::PixelFormat>();
}
- В методе
CameraSurface::start
проверяем формат изображения. Запускаем камеру, если формат поддерживается, иначе обрабатываем ошибку.
qcameracapture.cpp
...
bool CameraSurface::start(const QVideoSurfaceFormat& format)
{
if (!supportedPixelFormats(format.handleType()).contains(format.pixelFormat()))
{
qDebug() << format.handleType() << " " << format.pixelFormat() << " - format is not supported.";
return false;
}
return QAbstractVideoSurface::start(format);
}
- В методе
CameraSurface::present
обрабатываем новый кадр. Если кадр прошел проверку, то посылается сигнал обновления кадраframeUpdatedSignal
. Далее с этим сигналом будет связан слотframeUpdatedSlot
, где кадр будет обработан.
qcameracapture.cpp
...
bool CameraSurface::present(const QVideoFrame& frame)
{
if (!frame.isValid())
{
return false;
}
emit frameUpdatedSignal(frame);
return true;
}
- Конструктор класса
QCameraCapture
принимает указатель на родительский виджет (parent
), id камеры и разрешение изображения (ширина и высота), которые будут сохранены в соответствующие поля класса.
qcameracapture.h
class QCameraCapture : public QObject
{
Q_OBJECT
public:
explicit QCameraCapture(
QWidget* parent,
const int cam_id,
const int res_width,
const int res_height);
...
private:
QObject* _parent;
int cam_id;
int res_width;
int res_height;
}
- Добавляем в класс
QCameraCapture
объекты камерыm_camera
иm_surface
.
qcameracapture.h
#include <QScopedPointer>
class QCameraCapture : public QObject
{
...
private:
...
QScopedPointer<QCamera> m_camera;
QScopedPointer<CameraSurface> m_surface;
};
- Подключаем заголовочный файл
stdexcept
в файлеqcameracapture.cpp
для генерации исключений. В списке инициализации конструктораQCameraCapture::QCameraCapture
сохраняем указатель на родительский виджет, id камеры и разрешение изображения. В теле конструктора получаем список доступных камер. Список камер должен содержать хотя бы одну камеру, в противном случае будет выброшено исключениеruntime_error
. Также проверяем, что камера с запрашиваемым id есть в списке. Создаем камеру и подключаем сигналы камеры к слотам-обработчикам объекта. При изменении статуса камера посылает сигналstatusChanged
. Создаем объектCameraSurface
для отрисовки кадров с камеры. Подключаем сигналCameraSurface::frameUpdatedSignal
к слотуQCameraCapture::frameUpdatedSlot
.
qcameracapture.cpp
#include <QCameraInfo>
#include <stdexcept>
...
QCameraCapture::QCameraCapture(
QWidget* parent,
const int cam_id,
const int res_width,
const int res_height) : QObject(parent),
_parent(parent),
cam_id(cam_id),
res_width(res_width),
res_height(res_height)
{
const QList<QCameraInfo> availableCameras = QCameraInfo::availableCameras();
qDebug() << "Available cameras:";
for (const QCameraInfo &cameraInfo : availableCameras)
{
qDebug() << cameraInfo.deviceName() << " " << cameraInfo.description();
}
if(availableCameras.empty())
{
throw std::runtime_error("List of available cameras is empty");
}
if (!(cam_id >= 0 && cam_id < availableCameras.size()))
{
throw std::runtime_error("Invalid camera index");
}
const QCameraInfo cameraInfo = availableCameras[cam_id];
m_camera.reset(new QCamera(cameraInfo));
connect(m_camera.data(), &QCamera::statusChanged, this, &QCameraCapture::onStatusChanged);
connect(m_camera.data(), QOverload<QCamera::Error>::of(&QCamera::error), this, &QCameraCapture::cameraError);
m_surface.reset(new CameraSurface());
m_camera->setViewfinder(m_surface.data());
connect(m_surface.data(), &CameraSurface::frameUpdatedSignal, this, &QCameraCapture::frameUpdatedSlot);
}
- В деструкторе
QCameraCapture
останавливаем камеру.
qcameracapture.h
class QCameraCapture : public QObject
{
...
explicit QCameraCapture(...);
virtual ~QCameraCapture();
...
}
qcameracapture.cpp
QCameraCapture::~QCameraCapture()
{
if (m_camera)
{
m_camera->stop();
}
}
- Добавляем метод
QCameraCapture::frameUpdatedSlot
‒ обработчик сигналаCameraSurface::frameUpdatedSignal
. В этом методе мы конвертируем объектQVideoFrame
вQImage
и отправляем сигнал о том, что доступен новый кадр. СоздаемFramePtr
– указатель на изображение. Если изображение получено в формате RGB32, преобразуем его в RGB888.
qcameracapture.h
#include <memory>
class QCameraCapture : public QObject
{
Q_OBJECT
public:
typedef std::shared_ptr<QImage> FramePtr;
...
signals:
void newFrameAvailable();
...
public slots:
void frameUpdatedSlot(const QVideoFrame&);
...
}
qcameracapture.cpp
void QCameraCapture::frameUpdatedSlot(
const QVideoFrame& frame)
{
QVideoFrame cloneFrame(frame);
cloneFrame.map(QAbstractVideoBuffer::ReadOnly);
if (cloneFrame.pixelFormat() == QVideoFrame::Format_RGB24 ||
cloneFrame.pixelFormat() == QVideoFrame::Format_RGB32)
{
QImage image((const uchar*)cloneFrame.bits(),
cloneFrame.width(),
cloneFrame.height(),
QVideoFrame::imageFormatFromPixelFormat(cloneFrame.pixelFormat()));
if (image.format() == QImage::Format_RGB32)
{
image = image.convertToFormat(QImage::Format_RGB888);
}
FramePtr frame = FramePtr(new QImage(image));
}
cloneFrame.unmap();
emit newFrameAvailable();
}
- Добавляем методы запуска (
start
) и остановки (stop
) камерыQCameraCapture
.
qcameracapture.h
class QCameraCapture : public QObject
{
...
public:
...
void start();
void stop();
...
}
qcameracapture.cpp
void QCameraCapture::start()
{
m_camera->start();
}
void QCameraCapture::stop()
{
m_camera->stop();
}
- В методе
QCameraCapture::onStatusChanged
обрабатываем изменение статуса камеры наLoadedStatus
. Проверяем, поддерживает ли камера запрашиваемое разрешение. Устанавливаем запрашиваемое разрешение, если оно поддерживается камерой, иначе устанавливается разрешение по умолчанию (640 x 480), заданное статическими полями классаdefault_res_width
,default_res_height
.
qcameracapture.h
class QCameraCapture {
...
private slots:
void onStatusChanged();
...
private:
static const int default_res_width;
static const int default_res_height;
...
}
qcameracapture.cpp
const int QCameraCapture::default_res_width = 640;
const int QCameraCapture::default_res_height = 480;
...
void QCameraCapture::onStatusChanged()
{
if (m_camera->status() == QCamera::LoadedStatus)
{
bool found = false;
const QList<QSize> supportedResolutions = m_camera->supportedViewfinderResolutions();
for (const QSize &resolution : supportedResolutions)
{
if (resolution.width() == res_width &&
resolution.height() == res_height)
{
found = true;
}
}
if (!found)
{
qDebug() << "Resolution: " << res_width << "x" << res_width << " unsupported";
qDebug() << "Set default resolution: " << default_res_width << "x" << default_res_height;
res_width = default_res_width;
res_height = default_res_height;
}
QCameraViewfinderSettings viewFinderSettings;
viewFinderSettings.setResolution(
res_width,
res_height);
m_camera->setViewfinderSettings(viewFinderSettings);
}
}
- В методе
cameraError
выводим сообщение об ошибках камеры, если они возникают.
qcameracapture.h
class QCameraCapture : public QObject
{
...
private slots:
...
void cameraError();
...
}
qcameracapture.cpp
void QCameraCapture::cameraError()
{
qDebug() << "Camera error: " << m_camera->errorString();
}
- Создаем новый класс
Worker
: Add New > C++ > C++ Class > Choose… > Class name - Worker > Next > Finish. КлассWorker
через методaddFrame
будет сохранять последний кадр с камеры и отдавать этот кадр через методgetDataToDraw
.
worker.h
#include "qcameracapture.h"
#include <mutex>
class Worker
{
public:
// Данные для отрисовки
struct DrawingData
{
bool updated;
QCameraCapture::FramePtr frame;
DrawingData() : updated(false)
{
}
};
Worker();
void addFrame(QCameraCapture::FramePtr frame);
void getDataToDraw(DrawingData& data);
private:
DrawingData _drawing_data;
std::mutex _drawing_data_mutex;
};
worker.cpp
void Worker::getDataToDraw(DrawingData &data)
{
const std::lock_guard<std::mutex> guard(_drawing_data_mutex);
data = _drawing_data;
_drawing_data.updated = false;
}
void Worker::addFrame(QCameraCapture::FramePtr frame)
{
const std::lock_guard<std::mutex> guard(_drawing_data_mutex);
_drawing_data.frame = frame;
_drawing_data.updated = true;
}
- Отображение кадров будет выполняться в классе
ViewWindow
. Создаем виджет ViewWindow: Add > New > Qt > Designer Form Class > Choose... > Template > Widget (настройки по умолчанию) > Next > Name – ViewWindow > Project Management (настройки по умолчанию) > Finish. - В редакторе (Design) перетаскиваем на виджет объект Grid Layout: вызываем у виджета ViewWindow контекстное меню правым кликом и выбираем Lay out > Lay Out in a Grid. Объект Grid Layout позволяет размещать виджеты в сетке, он растягивается по размерам виджета ViewWindow. Далее добавляем на gridLayout объект Label и на панели свойств задаем ему имя frame: QObject > objectName > frame.
- Удаляем текст по умолчанию в QLabel > text.
- В класс
ViewWindow
добавляем камеру_qCamera
и инициализируем ее в конструкторе. Через статические поляcamera_image_width
иcamera_image_height
задаем требуемое разрешение изображения 1280x720. Флаг_running
хранит состояние запуска камеры:true
- камера запущена,false
- не запущена (остановлена).
viewwindow.h
#include "qcameracapture.h"
#include <QWidget>
namespace Ui {
class ViewWindow;
}
class ViewWindow : public QWidget
{
Q_OBJECT
public:
explicit ViewWindow(QWidget *parent);
~ViewWindow();
private:
Ui::ViewWindow *ui;
static const int camera_image_width;
static const int camera_image_height;
QScopedPointer<QCameraCapture> _qCamera;
bool _running;
};
viewwindow.cpp
const int ViewWindow::camera_image_width = 1280;
const int ViewWindow::camera_image_height = 720;
ViewWindow::ViewWindow(QWidget *parent) :
QWidget(parent),
ui(new Ui::ViewWindow())
{
ui->setupUi(this);
_running = false;
_qCamera.reset(new QCameraCapture(
this,
0, // id камеры
camera_image_width,
camera_image_height));
}
- Добавляем в класс
ViewWindow
объектWorker
и инициализируем его в конструкторе.
viewwindow.h
#include "worker.h"
class ViewWindow : public QWidget
{
...
private:
...
std::shared_ptr<Worker> _worker;
};
viewwindow.cpp
ViewWindow::ViewWindow(QWidget *parent) :
QWidget(parent),
ui(new Ui::ViewWindow())
{
...
_worker = std::make_shared<Worker>();
...
}
- Кадры будут подаваться в
Worker
изQCameraCapture
. Модифицируем классыQCameraCapture
иViewWindow
.
qcameracapture.h
...
class Worker;
class QCameraCapture : public QObject
{
...
public:
...
QCameraCapture(
...
std::shared_ptr<Worker> worker,
...);
...
private:
...
std::shared_ptr<Worker> _worker;
...
};
qcameracapture.cpp
#include "worker.h"
...
QCameraCapture::QCameraCapture(
...
std::shared_ptr<Worker> worker,
...) :
_worker(worker),
...
void QCameraCapture::frameUpdatedSlot(
const QVideoFrame& frame)
{
if (cloneFrame.pixelFormat() == QVideoFrame::Format_RGB24 ||
cloneFrame.pixelFormat() == QVideoFrame::Format_RGB32)
{
...
FramePtr frame = FramePtr(new QImage(image));
_worker->addFrame(frame);
}
...
}
viewwindow.cpp
ViewWindow::ViewWindow(QWidget *parent) :
QWidget(parent),
ui(new Ui::ViewWindow())
{
...
_qCamera.reset(new QCameraCapture(
this,
_worker,
...));
}
- Сигнал о появлении нового кадра
QCameraCapture::newFrameAvailable
обрабатывается в слотеViewWindow::draw
, который выводит изображение с камеры на виджет кадров.
viewwindow.h
class ViewWindow : public QWidget
{
...
private slots:
void draw();
...
}
viewwindow.cpp
ViewWindow::ViewWindow(QWidget *parent) :
QWidget(parent),
ui(new Ui::ViewWindow())
{
...
connect(_qCamera.data(), &QCameraCapture::newFrameAvailable, this, &ViewWindow::draw);
}
void ViewWindow::draw()
{
Worker::DrawingData data;
_worker->getDataToDraw(data);
// Если данные те же самые, отрисовка не производится
if(!data.updated)
{
return;
}
// Отрисовка
const QImage image = data.frame->copy();
ui->frame->setPixmap(QPixmap::fromImage(image));
}
- В методе
runProcessing
запускаем камеру, в методеstopProcessing
– останавливаем.
viewwindow.h
class ViewWindow : public QWidget
{
public:
...
void runProcessing();
void stopProcessing();
...
}
viewwindow.cpp
void ViewWindow::stopProcessing()
{
if (!_running)
return;
_qCamera->stop();
_running = false;
}
void ViewWindow::runProcessing()
{
if (_running)
return;
_qCamera->start();
_running = true;
}
- Останавливаем камеру в деструкторе
~ViewWindow
.
viewwindow.cpp
ViewWindow::~ViewWindow()
{
stopProcessing();
delete ui;
}
- Подключаем виджет камеры к главному окну приложения: создаем окно просмотра и запускаем обработку в конструкторе
MainWindow
. В деструкторе~MainWindow
останавливаем обработку.
mainwindow.h
...
class ViewWindow;
class MainWindow : public QMainWindow
{
...
private:
Ui::MainWindow *ui;
QScopedPointer<ViewWindow> _view;
};
mainwindow.cpp
#include "viewwindow.h"
#include <QMessageBox>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
try
{
_view.reset(new ViewWindow(this));
ui->viewLayout->addWidget(_view.data());
_view->runProcessing();
}
catch(std::exception &ex)
{
QMessageBox::critical(
this,
"Initialization Error",
ex.what());
throw;
}
}
MainWindow::~MainWindow()
{
if (_view)
{
_view->stopProcessing();
}
delete ui;
}
...
- Модифицируем функцию
main
для перехвата возможных исключений.
main.cpp
#include <QDebug>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
try
{
MainWindow w;
w.show();
return a.exec();
}
catch(std::exception& ex)
{
qDebug() << "Exception caught: " << ex.what();
}
catch(...)
{
qDebug() << "Unknown exception caught";
}
return 0;
}
- Запустите проект. Должно появиться окно с изображением с камеры.
Примечание: При запуске проекта на Windows с некоторыми камерами изображение может перевернуто или отзеркалено, это связано с особенностями обработки изображений Qt. В этом случае Вам потребуется дополнительно обработать изображение, например, при помощи QImage::mirrored().
Детектим и отслеживаем лица на видео
- Скачайте и распакуйте дистрибутив Face SDK, как это описано в пункте Приступая к работе. В корневой папке дистрибутива должны находиться папки bin и lib, соответствующие Вашей платформе.
- Для того, чтобы осуществлять детекцию и трекинг лиц на изображении с камеры, нам необходимо интегрировать Face SDK в наш проект. Указываем путь до корневой папки дистрибутива Face SDK (там находятся нужные нам заголовочные файлы) в переменной
FACE_SDK_PATH
в pro-файле проекта. Также указываем путь до папкиinclude
. В случае, если пути не прописаны, будет выводиться ошибка “Empty path to Face SDK”.
detection_and_tracking_with_video_worker.pro
...
# Set path to FaceSDK root directory
FACE_SDK_PATH =
isEmpty(FACE_SDK_PATH) {
error("Empty path to Face SDK")
}
DEFINES += FACE_SDK_PATH=\\\"$$FACE_SDK_PATH\\\"
INCLUDEPATH += $${FACE_SDK_PATH}/include
...
Примечание: При написании пути до Face SDK используйте слэш ("/").
- [Для Linux] Для сборки проекта добавляем в pro-файл следующую опцию:
detection_and_tracking_with_video_worker.pro
...
unix: LIBS += -ldl
...
- Кроме того, требуется указать путь до библиотеки facerec и конфигурационных файлов. Создадим класс
FaceSdkParameters
для хранения конфигурации (Add New > C++ > C++ Header File > FaceSdkParameters) и используем его вMainWindow
.
facesdkparameters.h
#include <string>
// Обработка и настройки Face SDK
struct FaceSdkParameters
{
std::string face_sdk_path = FACE_SDK_PATH;
};
mainwindow.h
#include <QMainWindow>
#include "facesdkparameters.h"
...
class MainWindow : public QMainWindow
{
private:
...
FaceSdkParameters _face_sdk_parameters;
}
- Подключаем Face SDK: добавляем заголовочные файлы в
mainwindow.h
и методinitFaceSdkService
для инициализации сервисов Face SDK. Создаем объектFacerecService
– компонент для создания модулей Face SDK, вызывая статический методFacerecService::createService
, передаем путь до библиотеки и путь до директории с конфигурационными файлами в try-catch блоке, чтобы перехватить возможные исключения. В случае успешной инициализации функцияinitFaceSdkService
вернетtrue
, иначе появится окно с ошибкой и вернетсяfalse
.
mainwindow.h
#include <facerec/libfacerec.h>
class MainWindow : public QMainWindow
{
...
private:
bool initFaceSdkService();
private:
...
pbio::FacerecService::Ptr _service;
...
}
mainwindow.cpp
bool MainWindow::initFaceSdkService()
{
// Подключаем Face SDK
QString error;
try
{
#ifdef _WIN32
std::string facerec_lib_path = _face_sdk_parameters.face_sdk_path + "/bin/facerec.dll";
#else
std::string facerec_lib_path = _face_sdk_parameters.face_sdk_path + "/lib/libfacerec.so";
#endif
_service = pbio::FacerecService::createService(
facerec_lib_path,
_face_sdk_parameters.face_sdk_path + "/conf/facerec");
return true;
}
catch(const std::exception &e)
{
error = tr("Can't init Face SDK service: '") + e.what() + "'.";
}
catch(...)
{
error = tr("Can't init Face SDK service: ... exception.");
}
QMessageBox::critical(
this,
tr("Face SDK error"),
error + "\n" +
tr("Try to change face sdk parameters."));
return false;
}
- В конструкторе
MainWindow::MainWindow
добавляем вызов инициализации сервиса. В случае ошибки выбрасываем исключениеstd::runtime_error
.
mainwindow.cpp
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
...
if (!initFaceSdkService())
{
throw std::runtime_error("Face SDK initialization error");
}
...
}
- Добавим передачу сервиса
FacerecService
и параметров Face SDK в конструкторViewWindow
, где они будут использованы для создания модуля трекингаVideoWorker
. В поля класса сохраняем сервис и параметры.
mainwindow.cpp
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
...
_view.reset(new ViewWindow(
this,
_service,
_face_sdk_parameters));
...
}
viewwindow.h
#include "facesdkparameters.h"
#include <facerec/libfacerec.h>
...
class ViewWindow : public QWidget
{
Q_OBJECT
public:
ViewWindow(
QWidget *parent,
pbio::FacerecService::Ptr service,
FaceSdkParameters face_sdk_parameters);
...
private:
...
pbio::FacerecService::Ptr _service;
FaceSdkParameters _face_sdk_parameters;
};
viewwindow.cpp
...
ViewWindow::ViewWindow(
QWidget *parent,
pbio::FacerecService::Ptr service,
FaceSdkParameters face_sdk_parameters) :
QWidget(parent),
ui(new Ui::ViewWindow()),
_service(service),
_face_sdk_parameters(face_sdk_parameters)
...
- Модифицируем класс
Worker
для работы с Face SDK. КлассWorker
будет принимать указатель на объектFacerecService
и имя конфигурационного файла модуля трекинга. КлассWorker
создает компонентVideoWorker
из Face SDK, осуществляющий трекинг лиц, подает ему кадры и обрабатывает коллбэки, в которых приходят результаты трекинга. Реализуем конструктор – в нем создаем объектVideoWorker
, указывая конфигурационный файл, метод распознавателя (в данном случае это пустая строка, т.к. распознавание лиц не используется), количество видеопотоков (в данном случае1
, т.к. используется только одна камера).
worker.h
#include <facerec/libfacerec.h>
class Worker
{
...
public:
...
Worker(
const pbio::FacerecService::Ptr service,
const std::string videoworker_config);
private:
...
pbio::VideoWorker::Ptr _video_worker;
};
worker.cpp
#include "worker.h"
#include "videoframe.h"
Worker::Worker(
const pbio::FacerecService::Ptr service,
const std::string videoworker_config)
{
pbio::FacerecService::Config vwconfig(videoworker_config);
_video_worker = service->createVideoWorker(
vwconfig,
"", // Распознавание не производится
1, // streams_count
0, // processing_threads_count
0); // matching_threads_count
}
Примечание: Помимо детекции и трекинга лиц объект VideoWorker
может использоваться для распознавания лиц на нескольких потоках. В этом случае указывается метод распознавателя и потоки processing_threads_count
и matching_threads_count
.
- Подписываемся на коллбэки от класса
VideoWorker
–TrackingCallback
(лицо найдено и отслеживается),TrackingLostCallback
(потеря трекинга лица) – и удаляем их в деструкторе.
worker.h
class Worker
{
public:
...
Worker(...);
~Worker();
...
private:
...
static void TrackingCallback(
const pbio::VideoWorker::TrackingCallbackData &data,
void* const userdata);
static void TrackingLostCallback(
const pbio::VideoWorker::TrackingLostCallbackData &data,
void* const userdata);
int _tracking_callback_id;
int _tracking_lost_callback_id;
};
worker.cpp
Worker::Worker(...)
{
...
_tracking_callback_id =
_video_worker->addTrackingCallbackU(
TrackingCallback,
this);
_tracking_lost_callback_id =
_video_worker->addTrackingLostCallbackU(
TrackingLostCallback,
this);
}
Worker::~Worker()
{
_video_worker->removeTrackingCallback(_tracking_callback_id);
_video_worker->removeTrackingLostCallback(_tracking_lost_callback_id);
}
- Для обработки исключений подключаем заголовочный файл
cassert
. В коллбэкеTrackingCallback
результат приходит в виде структурыTrackingCallbackData
, которая хранит данные обо всех отслеживаемых лицах. Вывод превью синхронизируется с выводом результата. Мы не можем сразу вывести кадр, который отдается вVideoWorker
, потому что он будет обработан чуть позже. Поэтому кадры складываются в очередь, и при получении результата мы можем найти кадр, который соответствует этому результату. Часть кадров может быть пропущена объектомVideoWorker
при большой нагрузке, поэтому не всегда для каждого кадра есть результат. В алгоритме ниже из очереди извлекается изображение, соответствующее последнему полученному результату. Сохраняем найденные лица для кадра, чтобы потом их использовать при визуализации. Для синхронизации изменения общих данных вTrackingCallback
иTrackingLostCallback
используются мьютексыstd::mutex
.
worker.h
#include <cassert>
...
class Worker
{
...
public:
// Информация о лице
struct FaceData
{
pbio::RawSample::Ptr sample;
bool lost;
int frame_id;
FaceData() : lost(true)
{
}
};
// Данные для отрисовки
struct DrawingData
{
...
int frame_id;
// map<track_id , face>
std::map<int, FaceData> faces;
...
};
...
};
worker.cpp
...
// static
void Worker::TrackingCallback(
const pbio::VideoWorker::TrackingCallbackData &data,
void * const userdata)
{
// Проверка аргументов
assert(userdata);
// frame_id - на каком кадре происходит отрисовка, samples - данные о лицах, которые будут отрисовываться
const int frame_id = data.frame_id;
const std::vector<pbio::RawSample::Ptr> &samples = data.samples;
// Информация о пользователе - указатель на Worker
// Передаем указатель
Worker &worker = *reinterpret_cast<Worker*>(userdata);
// Получить кадр с id, равным frame_id
QCameraCapture::FramePtr frame;
{
const std::lock_guard<std::mutex> guard(worker._frames_mutex);
auto& frames = worker._frames;
// Поиск в worker._frames
for(;;)
{
// Кадры уже должны быть получены
assert(frames.size() > 0);
// Проверяем, что frame_id приходят в порядке возрастания
assert(frames.front().first <= frame_id);
if(frames.front().first == frame_id)
{
// Кадр найден
frame = frames.front().second;
frames.pop();
break;
}
else
{
// Кадр был пропущен(i.e. the worker._frames.front())
std::cout << "skipped " << ":" << frames.front().first << std::endl;
frames.pop();
}
}
}
// Обновляем информацию
{
const std::lock_guard<std::mutex> guard(worker._drawing_data_mutex);
// Кадр
worker._drawing_data.frame = frame;
worker._drawing_data.frame_id = frame_id;
worker._drawing_data.updated = true;
// Сэмплы
for(size_t i = 0; i < samples.size(); ++i)
{
FaceData &face = worker._drawing_data.faces[samples[i]->getID()];
face.frame_id = samples[i]->getFrameID(); // Может отличаться от frame_id
face.lost = false;
face.sample = samples[i];
}
}
}
- Реализуем коллбэк
TrackingLostCallback
, в котором помечаем, что трекаемое лицо пропало из кадра.
worker.cpp
...
// static
void Worker::TrackingLostCallback(
const pbio::VideoWorker::TrackingLostCallbackData &data,
void* const userdata)
{
const int track_id = data.track_id;
// Информация о пользователе - указатель на Worker
// Передаем указатель
Worker &worker = *reinterpret_cast<Worker*>(userdata);
{
const std::lock_guard<std::mutex> guard(worker._drawing_data_mutex);
FaceData &face = worker._drawing_data.faces[track_id];
assert(!face.lost);
face.lost = true;
}
}
- Объект
VideoWorker
принимает кадры через интерфейсpbio::IRawImage
. Создадим заголовочный файлVideoFrame
: Add New > C++ > C++ Header File > VideoFrame. Подключаем интерфейсpbio::IRawImage
в файлеvideoframe.h
и реализуем этот интерфейс для классаQImage
. Интерфейсpbio::IRawImage
позволяет получить указатель на данные изображения, его формат, высоту и ширину.
videoframe.h
Нажмите, чтобы развернуть
#include "qcameracapture.h"
#include <pbio/IRawImage.h>
class VideoFrame : public pbio::IRawImage
{
public:
VideoFrame(){}
virtual ~VideoFrame(){}
virtual const unsigned char* data() const throw();
virtual int32_t width() const throw();
virtual int32_t height() const throw();
virtual int32_t format() const throw();
QCameraCapture::FramePtr& frame();
const QCameraCapture::FramePtr& frame() const;
private:
QCameraCapture::FramePtr _frame;
};
inline
const unsigned char* VideoFrame::data() const throw()
{
if(_frame->isNull() || _frame->size().isEmpty())
{
return NULL;
}
return _frame->bits();
}
inline
int32_t VideoFrame::width() const throw()
{
return _frame->width();
}
inline
int32_t VideoFrame::height() const throw()
{
return _frame->height();
}
inline
int32_t VideoFrame::format() const throw()
{
if(_frame->format() == QImage::Format_Grayscale8)
{
return (int32_t) FORMAT_GRAY;
}
if(_frame->format() == QImage::Format_RGB888)
{
return (int32_t) FORMAT_RGB;
}
return -1;
}
inline
QCameraCapture::FramePtr& VideoFrame::frame()
{
return _frame;
}
inline
const QCameraCapture::FramePtr& VideoFrame::frame() const
{
return _frame;
}
- В методе
addFrame
подаем кадры вVideoWorker
. Если при обработке коллбэков возникают исключения, они выбрасываются повторно в методеcheckExceptions
. Для хранения кадров заведем очередь_frames
, в которую будем складывать id кадра и соответствующее изображение, чтобы в коллбэкеTrackingCallback
найти кадр, соответствующий результату обработки. Для синхронизации изменения общих данных также используются мьютексыstd::mutex
.
worker.h
#include <queue>
...
class Worker : public QObject
{
...
private:
...
std::queue<std::pair<int, QCameraCapture::FramePtr> > _frames;
std::mutex _frames_mutex;
...
};
worker.cpp
#include "videoframe.h"
...
void Worker::addFrame(
QCameraCapture::FramePtr frame)
{
VideoFrame video_frame;
video_frame.frame() = frame;
const std::lock_guard<std::mutex> guard(_frames_mutex);
const int stream_id = 0;
const int frame_id = _video_worker->addVideoFrame(
video_frame,
stream_id);
_video_worker->checkExceptions();
_frames.push(std::make_pair(frame_id, video_frame.frame()));
}
- Модифицируем метод
getDataToDraw
, чтобы не рисовать лица, для которых был вызванTrackingLostCallback
.
worker.cpp
void Worker::getDataToDraw(DrawingData &data)
{
...
// Удаляем сэмплы, для которых был вызван TrackingLostCallback
{
for(auto it = _drawing_data.faces.begin();
it != _drawing_data.faces.end();)
{
const std::map<int, FaceData>::iterator i = it;
++it; // i может быть удален, поэтому инкрементируем его на данном этапе
FaceData &face = i->second;
// Оставляем трекаемые лица
if(!face.lost)
continue;
_drawing_data.updated = true;
// Удаляем лица
_drawing_data.faces.erase(i);
}
}
...
}
- Модифицируем класс
QCameraCapture
для перехвата исключений, которые могут возникнуть вWorker::addFrame
.
qcameracapture.cpp
#include <QMessageBox>
...
void QCameraCapture::frameUpdatedSlot(
const QVideoFrame& frame)
{
...
if (cloneFrame.pixelFormat() == QVideoFrame::Format_RGB24 ||
cloneFrame.pixelFormat() == QVideoFrame::Format_RGB32)
{
QImage image(...);
if (image.format() == QImage::Format_RGB32)
{
image = image.convertToFormat(QImage::Format_RGB888);
}
try
{
FramePtr frame = FramePtr(new QImage(image));
_worker->addFrame(frame);
}
catch(std::exception& ex)
{
stop();
cloneFrame.unmap();
QMessageBox::critical(
_parent,
tr("Face SDK error"),
ex.what());
return;
}
}
...
}
- Создадим класс
DrawFunction
, который будет содержать метод для отрисовки результатов трекинга на изображении: Add New > C++ > C++ Class > Choose… > Class name – DrawFunction.
drawfunction.h
#include "worker.h"
class DrawFunction
{
public:
DrawFunction();
static QImage Draw(
const Worker::DrawingData &data);
};
drawfunction.cpp
#include "drawfunction.h"
#include <QPainter>
DrawFunction::DrawFunction()
{
}
// static
QImage DrawFunction::Draw(
const Worker::DrawingData &data)
{
QImage result = data.frame->copy();
QPainter painter(&result);
// Клонируем информацию о лицах
std::vector<std::pair<int, Worker::FaceData> > faces_data(data.faces.begin(), data.faces.end());
// Рисуем лица
for(const auto &face_data : faces_data)
{
const Worker::FaceData &face = face_data.second;
painter.save();
// Визуализация лица в кадре
if(face.frame_id == data.frame_id && !face.lost)
{
const pbio::RawSample& sample = *face.sample;
QPen pen;
// Отрисовка ограничивающего прямоугольника лица
{
// Получаем ограничивающий прямоугольник лица
const pbio::RawSample::Rectangle bounding_box = sample.getRectangle();
pen.setWidth(3);
pen.setColor(Qt::green);
painter.setPen(pen);
painter.drawRect(bounding_box.x, bounding_box.y, bounding_box.width, bounding_box.height);
}
}
painter.restore();
}
painter.end();
return result;
}
- В конструкторе
ViewWindow
передаем указатель на объектFacerecService
и имя конфигурационного файла модуля трекинга при созданииWorker
. В методеDraw
рисуем результаты трекинга на изображении через вызовDrawFunction::Draw
.
viewwindow.cpp
#include "drawfunction.h"
...
ViewWindow::ViewWindow(...) :
...
{
...
_worker = std::make_shared<Worker>(
_service,
"video_worker_lbf.xml");
...
}
...
void ViewWindow::draw()
{
...
// Отрисовка
const QImage image = DrawFunction::Draw(data);
...
}
- Запустите проект. Теперь на изображении с камеры детектятся и отслеживаются лица (они выделяются зеленым прямугольником). Больше информации об использовании объекта
VideoWorker
вы можете найти в разделе Обработка видеопотоков.