Приступаем к разбору кода.
Как обычно вначале подключаем все нужные библиотеки:
Код:
#include <Windows.h>
#include <stdio.h>
// Подключение библиотек Kinect
#include <Shlobj.h>
#include <NuiApi.h>
//
#include < iostream >
using namespace std;
// Подключение библиотек OpenСV
#include "opencv2\opencv.hpp"
//
Просто копируем этот участок кода в ваш проект.
Дальше объявляем все переменные:
Код:
static const int cDepthWidth = 640;
static const int cDepthHeight = 480;
static const int cBytesPerPixel = 1;
static const NUI_IMAGE_RESOLUTION cColorResolution = NUI_IMAGE_RESOLUTION_640x480;
LARGE_INTEGER m_colorTimeStamp; // таймер камеры
LARGE_INTEGER m_depthTimeStamp; // таймер глубины
BYTE* m_colorRGBX;
INuiSensor* m_pNuiSensor = NULL;
HANDLE m_pDepthStreamHandle = NULL;
HANDLE m_hNextDepthFrameEvent = INVALID_HANDLE_VALUE; // событие сигнализирующнн о готовности данных глубины
HANDLE m_pColorStreamHandle;
HANDLE m_hNextColorFrameEvent; // событие сигнализирующее о готовности видео
USHORT* m_depthRGBX;
LONG* m_colorCoordinates;
HANDLE m_hNextSkeletonEvent = NULL; // событие сигнализирующее о готовности данных скелета
DWORD m_SkeletonTrackingFlags;
HRESULT CreateFirstConnected(); // функция подключения к Kinect
HRESULT GetColor(); // функция получения видеопотока с камеры
HRESULT GetDepth(); // функция получение потока глубины (с IR камеры)
bool GetSkeleton(); // функция получения скелетона
void Nui_DrawSkeleton(const NUI_SKELETON_DATA & skel, int windowWidth, int windowHeight); // функция отрисовки скелетона
void UpdateTrackedSkeletons( const NUI_SKELETON_FRAME & skel ); // функция определяет какие скелеты отслеживать и отслеживает их
IplImage* ImageVideo = cvCreateImage( cvSize( 640, 480 ), 8, 3 ); // картинка для видео с камеры
IplImage* ImageDepth = cvCreateImage( cvSize( 640, 480 ), 16, 1 ); // картинка для карты глубины
IplImage *ImageSkel = cvCreateImage(cvSize(640, 480), 8, 3); //картинка для скелетона
IplImage* Mask = cvCreateImage( cvSize( 640, 480 ), 8, 1 );
int g_ScreenWidth = 640;
int g_ScreenHeight = 480;
int m_StickySkeletonIds[5];
CvPoint2D32f m_Points[NUI_SKELETON_POSITION_COUNT]; // объявление точки в OpenCV
int humanFigure = 0; // счетчик фигур людей
struct Points {public: int x; int y;}; // создать структуру для хранения координат точек скелетона
Points head; // объявить координаты головы
Далее следует основной модуль программы в теле которого находится бесконечный цикл получающий и обрабатывающий данные с сенсора:
Код:
int main()
{
m_depthRGBX = new USHORT[cDepthWidth*cDepthHeight*cBytesPerPixel];
m_colorRGBX = new BYTE[cDepthWidth*cDepthHeight*4];
m_colorCoordinates = new LONG[640*480*2];
// Соединение с KINECT
CreateFirstConnected();
int i,j;
while (1)
{
// если готовы данные с камеры то получить их
if ( WAIT_OBJECT_0 == WaitForSingleObject(m_hNextColorFrameEvent, 0) )
GetColor();
// если имеется готовые данные о глубине то получить их
if ( WAIT_OBJECT_0 == WaitForSingleObject(m_hNextDepthFrameEvent, 0) )
GetDepth();
// если готовы данные скелета то получить их
if ( WAIT_OBJECT_0 == WaitForSingleObject(m_hNextSkeletonEvent, 0) )
GetSkeleton();
//printf ("head = %d X = %d Y = %d\n", humanFigure, head.x, head.y);
cvShowImage( "глубина", ImageDepth ); // показать карту глубины
cvShowImage( "видео", ImageVideo ); // показать картинку с камеры
cvShowImage( "скелет", ImageSkel ); // показать скелетон
//cvShowImage( "Mask", Mask ); // показать Mask
cvSet( Mask, cvScalar( 0 ) ); // сбросить Mask
// формирование кадра с камеры KINECT в openCV
for( i = 0; i < cDepthHeight; i ++ )
for( j = 0; j < cDepthWidth; j++ )
{
ImageVideo->imageData[i*ImageVideo->widthStep + j*3] = m_colorRGBX[i*4*cDepthWidth+j*4];
ImageVideo->imageData[i*ImageVideo->widthStep + j*3+1] = m_colorRGBX[i*4*cDepthWidth+j*4+1];
ImageVideo->imageData[i*ImageVideo->widthStep + j*3+2] = m_colorRGBX[i*4*cDepthWidth+j*4+2];
// сформировть фигуру(ы) человека и передать в струкуру Mask
USHORT player = NuiDepthPixelToPlayerIndex(m_depthRGBX[i*cDepthWidth+j]);
if ( player > 0 )
{
int j1 = m_colorCoordinates[(i*cDepthWidth+j)*2];
int i1 = m_colorCoordinates[(i*cDepthWidth+j)*2+1];
if ( j1>= 0 && j1 < cDepthWidth && i1 >= 0 && i1 < cDepthHeight )
{
Mask->imageData[i1*Mask->widthStep + j1] = (char)255;
}
}
}
memcpy( ImageDepth->imageData, m_depthRGBX, cDepthWidth*cDepthHeight*2 ); // сформировать карту глубины
if (cvWaitKey(1) == 27)
break; //Если Esc - выходим
}
// Закрытие
NuiShutdown();
if (m_pNuiSensor) {m_pNuiSensor->NuiShutdown();}
if (m_hNextDepthFrameEvent != INVALID_HANDLE_VALUE) {CloseHandle(m_hNextDepthFrameEvent);}
if (m_hNextColorFrameEvent != INVALID_HANDLE_VALUE) {CloseHandle(m_hNextColorFrameEvent);}
if (m_hNextSkeletonEvent != INVALID_HANDLE_VALUE) {CloseHandle(m_hNextSkeletonEvent);}
// удаление данных
delete[] m_depthRGBX;
cvReleaseImage( &ImageDepth );
m_pNuiSensor->Release();
return 0;
}
Функция получения видео с камеры:
Код:
// Получение видео потока с камеры
HRESULT GetColor()
{
HRESULT hr = S_OK;
NUI_IMAGE_FRAME imageFrame;
// получение кадра видео
hr = m_pNuiSensor->NuiImageStreamGetNextFrame(m_pColorStreamHandle, 0, &imageFrame);
if (FAILED(hr)) {return hr;} // возврат в случае не удачи получения кадра
m_colorTimeStamp = imageFrame.liTimeStamp;
INuiFrameTexture * pTexture = imageFrame.pFrameTexture;
NUI_LOCKED_RECT LockedRect;
// Блокировка кадра Kinect, чтобы они не изменялся во время чтения кадра
pTexture->LockRect(0, &LockedRect, NULL, 0);
// Убедиться в том, что мы получили достоверные данные
if (LockedRect.Pitch != 0) {memcpy(m_colorRGBX, LockedRect.pBits, LockedRect.size);}
// разблокировать текстуру
pTexture->UnlockRect(0);
// Освободить кадр
m_pNuiSensor->NuiImageStreamReleaseFrame(m_pColorStreamHandle, &imageFrame);
return hr;
}
Функция получения глубины:
Код:
// Получение потока глубины (с IR камеры)
HRESULT GetDepth()
{
HRESULT hr = S_OK;
NUI_IMAGE_FRAME imageFrame;
// получение кадра глубины
hr = m_pNuiSensor->NuiImageStreamGetNextFrame(m_pDepthStreamHandle, 0, &imageFrame);
if (FAILED(hr)) {return hr;} // возврат в случае не удачи получения кадра
m_depthTimeStamp = imageFrame.liTimeStamp;
INuiFrameTexture * pTexture = imageFrame.pFrameTexture;
NUI_LOCKED_RECT LockedRect;
// Блокировка кадра Kinect, чтобы они не изменялся во время чтения кадра
pTexture->LockRect(0, &LockedRect, NULL, 0);
// Убедиться в том, что мы получили достоверные данные
if (LockedRect.Pitch != 0) {memcpy(m_depthRGBX, LockedRect.pBits, LockedRect.size);}
// разблокировать текстуру
pTexture->UnlockRect(0);
// Освободить кадр
m_pNuiSensor->NuiImageStreamReleaseFrame(m_pDepthStreamHandle, &imageFrame);
return hr;
}
Получение скелетона:
Код:
// Получение скелетона
bool GetSkeleton()
{
NUI_SKELETON_FRAME SkeletonFrame = {0};
if (humanFigure >= 1)
{
cvSet( ImageSkel, cvScalar(0)); // очистить кадр со скелетоном
humanFigure = 0; // сбросить счетчик фигур
head.x = 0; head.y = 0; // сбросить координаты головы
}
bool foundSkeleton = false;
if ( SUCCEEDED(m_pNuiSensor->NuiSkeletonGetNextFrame( 0, &SkeletonFrame )) )
{
for ( int i = 0 ; i < NUI_SKELETON_COUNT ; i++ )
{
NUI_SKELETON_TRACKING_STATE trackingState = SkeletonFrame.SkeletonData[i].eTrackingState;
if ( trackingState == NUI_SKELETON_TRACKED || trackingState == NUI_SKELETON_POSITION_ONLY )
foundSkeleton = true;
}
}
if( !foundSkeleton ) return true; // выход если нет скелетонов
// сглаживание данных скелетона
HRESULT hr = m_pNuiSensor->NuiTransformSmooth(&SkeletonFrame,NULL);
if ( FAILED(hr) ) return false;
for ( int i = 0 ; i < NUI_SKELETON_COUNT; i++ )
{
NUI_SKELETON_TRACKING_STATE trackingState = SkeletonFrame.SkeletonData[i].eTrackingState;
if ( trackingState == NUI_SKELETON_TRACKED )
{
humanFigure = humanFigure++; // инкремент счетчика фигур людей
// нарисовать отслеживаемый скелет
Nui_DrawSkeleton( SkeletonFrame.SkeletonData[i], ImageSkel->width, ImageSkel->height );
}
}
UpdateTrackedSkeletons( SkeletonFrame );
return 1;
}
Обработка скелетонов:
Код:
// Нарисовать кости и суставы
void Nui_DrawBone( const NUI_SKELETON_DATA & skel, NUI_SKELETON_POSITION_INDEX bone0, NUI_SKELETON_POSITION_INDEX bone1 )
{
NUI_SKELETON_POSITION_TRACKING_STATE bone0State = skel.eSkeletonPositionTrackingState[bone0];
NUI_SKELETON_POSITION_TRACKING_STATE bone1State = skel.eSkeletonPositionTrackingState[bone1];
// выход, если суставы не обнаружены
if ( bone0State == NUI_SKELETON_POSITION_NOT_TRACKED || bone1State == NUI_SKELETON_POSITION_NOT_TRACKED )
return;
// не отрисовывать если обе точки совпадают
if ( bone0State == NUI_SKELETON_POSITION_INFERRED && bone1State == NUI_SKELETON_POSITION_INFERRED )
return;
// нарисовать зеленым цветом кости суставы которых отслеживаются, а красным суставы кости которых не видно
if ( bone0State == NUI_SKELETON_POSITION_TRACKED && bone1State == NUI_SKELETON_POSITION_TRACKED ) // если кость видима
{
cvLine( ImageSkel, cvPointFrom32f( m_Points[bone0]), cvPointFrom32f( m_Points[bone1]), CV_RGB(0, 128, 0), 2 ); // нарисовать кость
cvLine( ImageVideo, cvPointFrom32f( m_Points[bone0]), cvPointFrom32f( m_Points[bone1]), CV_RGB(0, 128, 0), 1 ); // нарисовать кость на видео
}
else // если кость не видима
{
cvLine( ImageSkel, cvPointFrom32f( m_Points[bone0]), cvPointFrom32f( m_Points[bone1]), CV_RGB(128, 0, 0), 2 ); // нарисовать кость
cvLine( ImageVideo, cvPointFrom32f( m_Points[bone0]), cvPointFrom32f( m_Points[bone1]), CV_RGB(128, 0, 0), 1 ); // нарисовать кость на видео
}
}
// Точка в OpenCV
CvPoint2D32f SkeletonToScreen( Vector4 skeletonPoint, int width, int height )
{
LONG x, y;
USHORT depth;
// вычислить положение скелета на экране
// NuiTransformSkeletonToDepthImage возвращает координаты в разрешении NUI_IMAGE_RESOLUTION_320x240
NuiTransformSkeletonToDepthImage( skeletonPoint, &x, &y, &depth );
float screenPointX = static_cast<float>(x * width) / g_ScreenWidth * 2;
float screenPointY = static_cast<float>((y + 10) * height) / g_ScreenHeight * 2;
return cvPoint2D32f(screenPointX, screenPointY);
}
// Какие скелеты отслеживать и их отслеживание
void UpdateTrackedSkeletons( const NUI_SKELETON_FRAME & skel )
{
DWORD nearestIDs[2] = { 0, 0 };
USHORT nearestDepths[2] = { NUI_IMAGE_DEPTH_MAXIMUM, NUI_IMAGE_DEPTH_MAXIMUM };
// Очистить идентификаторы скелета, если пользователь покинул кадр
bool stickyID0Found = false;
bool stickyID1Found = false;
for ( int i = 0 ; i < NUI_SKELETON_COUNT; i++ )
{
NUI_SKELETON_TRACKING_STATE trackingState = skel.SkeletonData[i].eTrackingState;
if ( trackingState == NUI_SKELETON_TRACKED || trackingState == NUI_SKELETON_POSITION_ONLY )
{
if ( skel.SkeletonData[i].dwTrackingID == m_StickySkeletonIds[0] )
stickyID0Found = true;
else if ( skel.SkeletonData[i].dwTrackingID == m_StickySkeletonIds[1] )
stickyID1Found = true;
}
}
if ( !stickyID0Found && stickyID1Found )
{
m_StickySkeletonIds[0] = m_StickySkeletonIds[1];
m_StickySkeletonIds[1] = 0;
}
else if ( !stickyID0Found )
{
m_StickySkeletonIds[0] = 0;
}
else if ( !stickyID1Found )
{
m_StickySkeletonIds[1] = 0;
}
// рассчитать ближайший скелет
for ( int i = 0 ; i < NUI_SKELETON_COUNT; i++ )
{
NUI_SKELETON_TRACKING_STATE trackingState = skel.SkeletonData[i].eTrackingState;
if ( trackingState == NUI_SKELETON_TRACKED || trackingState == NUI_SKELETON_POSITION_ONLY )
{
// сохранить скелетон если нет ранее сохраненных
if ( 0 == m_StickySkeletonIds[0] && m_StickySkeletonIds[1] != skel.SkeletonData[i].dwTrackingID )
{
m_StickySkeletonIds[0] = skel.SkeletonData[i].dwTrackingID;
}
else if ( 0 == m_StickySkeletonIds[1] && m_StickySkeletonIds[0] != skel.SkeletonData[i].dwTrackingID )
{
m_StickySkeletonIds[1] = skel.SkeletonData[i].dwTrackingID;
}
LONG x, y;
USHORT depth;
// вычислить положение скелета на экране
NuiTransformSkeletonToDepthImage( skel.SkeletonData[i].Position, &x, &y, &depth );
if ( depth < nearestDepths[0] )
{
nearestDepths[1] = nearestDepths[0];
nearestIDs[1] = nearestIDs[0];
nearestDepths[0] = depth;
nearestIDs[0] = skel.SkeletonData[i].dwTrackingID;
}
else if ( depth < nearestDepths[1] )
{
nearestDepths[1] = depth;
nearestIDs[1] = skel.SkeletonData[i].dwTrackingID;
}
}
}
}
/// Нарисовать скелетон
void Nui_DrawSkeleton( const NUI_SKELETON_DATA & skel, int windowWidth, int windowHeight )
{
int i;
for (i = 0; i < NUI_SKELETON_POSITION_COUNT; i++)
{
m_Points[i] = SkeletonToScreen( skel.SkeletonPositions[i], windowWidth, windowHeight );
}
// отобразить торс
Nui_DrawBone( skel, NUI_SKELETON_POSITION_HEAD, NUI_SKELETON_POSITION_SHOULDER_CENTER );
Nui_DrawBone( skel, NUI_SKELETON_POSITION_SHOULDER_CENTER, NUI_SKELETON_POSITION_SHOULDER_LEFT );
Nui_DrawBone( skel, NUI_SKELETON_POSITION_SHOULDER_CENTER, NUI_SKELETON_POSITION_SHOULDER_RIGHT );
Nui_DrawBone( skel, NUI_SKELETON_POSITION_SHOULDER_CENTER, NUI_SKELETON_POSITION_SPINE );
Nui_DrawBone( skel, NUI_SKELETON_POSITION_SPINE, NUI_SKELETON_POSITION_HIP_CENTER );
Nui_DrawBone( skel, NUI_SKELETON_POSITION_HIP_CENTER, NUI_SKELETON_POSITION_HIP_LEFT );
Nui_DrawBone( skel, NUI_SKELETON_POSITION_HIP_CENTER, NUI_SKELETON_POSITION_HIP_RIGHT );
// левая рука
Nui_DrawBone( skel, NUI_SKELETON_POSITION_SHOULDER_LEFT, NUI_SKELETON_POSITION_ELBOW_LEFT );
Nui_DrawBone( skel, NUI_SKELETON_POSITION_ELBOW_LEFT, NUI_SKELETON_POSITION_WRIST_LEFT );
Nui_DrawBone( skel, NUI_SKELETON_POSITION_WRIST_LEFT, NUI_SKELETON_POSITION_HAND_LEFT );
// правая рука
Nui_DrawBone( skel, NUI_SKELETON_POSITION_SHOULDER_RIGHT, NUI_SKELETON_POSITION_ELBOW_RIGHT );
Nui_DrawBone( skel, NUI_SKELETON_POSITION_ELBOW_RIGHT, NUI_SKELETON_POSITION_WRIST_RIGHT );
Nui_DrawBone( skel, NUI_SKELETON_POSITION_WRIST_RIGHT, NUI_SKELETON_POSITION_HAND_RIGHT );
// левая нога
Nui_DrawBone( skel, NUI_SKELETON_POSITION_HIP_LEFT, NUI_SKELETON_POSITION_KNEE_LEFT );
Nui_DrawBone( skel, NUI_SKELETON_POSITION_KNEE_LEFT, NUI_SKELETON_POSITION_ANKLE_LEFT );
Nui_DrawBone( skel, NUI_SKELETON_POSITION_ANKLE_LEFT, NUI_SKELETON_POSITION_FOOT_LEFT );
// правая нога
Nui_DrawBone( skel, NUI_SKELETON_POSITION_HIP_RIGHT, NUI_SKELETON_POSITION_KNEE_RIGHT );
Nui_DrawBone( skel, NUI_SKELETON_POSITION_KNEE_RIGHT, NUI_SKELETON_POSITION_ANKLE_RIGHT );
Nui_DrawBone( skel, NUI_SKELETON_POSITION_ANKLE_RIGHT, NUI_SKELETON_POSITION_FOOT_RIGHT );
head.x = m_Points[NUI_SKELETON_POSITION_HEAD].x; // получить координату X головы
head.y = m_Points[NUI_SKELETON_POSITION_HEAD].y; // получить координату Y головы
// нарисовать суставы разным цветом
for ( i = 0; i < NUI_SKELETON_POSITION_COUNT; i++ )
{
if ( skel.eSkeletonPositionTrackingState[i] == NUI_SKELETON_POSITION_INFERRED ) // если сустав видим
{
cvCircle( ImageSkel, cvPointFrom32f( m_Points[i]), 5, CV_RGB(128, 0, 0), 3 ); // отобразить сустав
cvCircle( ImageVideo, cvPointFrom32f( m_Points[i]), 5, CV_RGB(128, 0, 0), 1 ); // отобразить сустав на видео
}
else if ( skel.eSkeletonPositionTrackingState[i] == NUI_SKELETON_POSITION_TRACKED ) // если сустав не видим
{
cvCircle( ImageSkel, cvPointFrom32f( m_Points[i]), 5, CV_RGB(0, 128, 0), 3 ); // отобразить сустав
cvCircle( ImageVideo, cvPointFrom32f( m_Points[i]), 5, CV_RGB(0, 128, 0), 1 ); // отобразить сустав на видео
}
}
// Отображать надпись при поднятии рук вверх
CvFont font;
float aa=1;
cvInitFont( &font, CV_FONT_HERSHEY_SIMPLEX, aa,aa,0,2, 8 );
i = 0;
if ( m_Points[NUI_SKELETON_POSITION_HAND_RIGHT].y < m_Points[NUI_SKELETON_POSITION_HEAD].y )
{
cvPutText( ImageSkel, "Right Hand Up", cvPoint(0,30), &font, CV_RGB(255,255,255) );
i++;
}
if ( m_Points[NUI_SKELETON_POSITION_HAND_LEFT].y < m_Points[NUI_SKELETON_POSITION_HEAD].y )
{
cvPutText( ImageSkel, "Left Hand Up", cvPoint(0,30+i*30), &font, CV_RGB(255,255,255) );
i++;
}
}
Соединение с сенсором:
Код:
// Соединение с сенсором
HRESULT CreateFirstConnected()
{
INuiSensor * pNuiSensor;
HRESULT hr;
int iSensorCount = 0;
hr = NuiGetSensorCount(&iSensorCount);
if (FAILED(hr))
return hr;
// Просматриваем каждый сенсор Kinect
for (int i = 0; i < iSensorCount; ++i)
{
// Создание датчика, для проверки состояния, если датчик не возможно создать, перейти на следующий
hr = NuiCreateSensorByIndex(i, &pNuiSensor);
if (FAILED(hr))
continue;
// Получить состояние сенсора, и если он подключен, то можно инициализировать его
hr = pNuiSensor->NuiStatus();
if (S_OK == hr)
{
m_pNuiSensor = pNuiSensor;
break;
}
// сенсор не работает, освобождаем его
pNuiSensor->Release();
}
if (NULL != m_pNuiSensor)
{
// Инициализировать Kinect и указать, что мы будем использовать глубину, скелет и камеру
hr = m_pNuiSensor -> NuiInitialize(
NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX | //глубина и индекс фигуры человека
NUI_INITIALIZE_FLAG_USES_SKELETON | //скелет
NUI_INITIALIZE_FLAG_USES_COLOR); //видео с камеры
if (SUCCEEDED(hr))
{
// Создать событие, которое будет сигнализировать, о том что данные глубины доступны (данные с IR камеры)
m_hNextDepthFrameEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
// Открытие потока глубины изображения для получения глубины кадра
hr = m_pNuiSensor->NuiImageStreamOpen(
NUI_IMAGE_TYPE_DEPTH_AND_PLAYER_INDEX,
NUI_IMAGE_RESOLUTION_640x480,
0,
2,
m_hNextDepthFrameEvent,
&m_pDepthStreamHandle);
// Создать событие, которое будет сигнализировать, о том что данные цвета доступны (данные с видео камеры)
if (SUCCEEDED(hr))
{
m_hNextColorFrameEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
// Открытие потока цветного изображения
hr = m_pNuiSensor->NuiImageStreamOpen(
NUI_IMAGE_TYPE_COLOR,
cColorResolution,
0,
2,
m_hNextColorFrameEvent,
&m_pColorStreamHandle);
}
// Создать событие, которое будет сигнализировать, о том что данные о скелете доступны
m_hNextSkeletonEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (HasSkeletalEngine(m_pNuiSensor)) // Поддерживает ли сенсор скелет
{
// Открытие потока скелета
hr = m_pNuiSensor -> NuiSkeletonTrackingEnable(
m_hNextSkeletonEvent,
m_SkeletonTrackingFlags);
if(FAILED(hr))
return hr;
}
}
}
if (NULL == m_pNuiSensor || FAILED(hr))
return E_FAIL;
return hr;
}
Вроде это все. Если есть вопросы спрашивайте. В коде практически к каждой строке имеется комментарий, внимательно проследите ход выполнения программы и тогда все станет более-менее понятно.
От этого кода вы можете оттолкнуться взяв его в свои проекты, развив и модернизировав его.
В нашем проекте
forum10/topic13841.html в код добавлен обмен данными через COM порт для управления внешними двигателями и получения информации от датчиков, введены параллельные потоки в программе (в связи с чем пришлось вводить критические секции), добавлено распознавание лиц и др.
Успехов!
