Готовый перевод Mastering OpenCV with Practical Computer Vision Projects / Опановуюємо OpenCV через практичні проекти комп'ютерного зору: Розділ 8. Розпізнавання обличчя за методами Eigenfaces та Fisherfaces

Внимание! Этот перевод, возможно, ещё не готов, так как модераторы установили для него статус «перевод редактируется»

У цьому розділі опишемо концепції пошуку та розпізнавання обличчя, а також створимо відповідний проєкт. Розпізнавання облич - це водночас популярна і складна тема, багато дослідників присвячують роки життя цій сфері. Але тут ми пояснимо лише прості методи розпізнавання, формуючи фундамент для подальшого вивчення більш складних.

У розділі покриємо такі теми:
Пошук обличчя
Попередня обробка обличчя
Тренування алгоритму машинного навчання на основі зібраних облич
Розпізнавання обличчя
Постобробка

Вступ до пошуку та розпізнавання обличчя
Розпізнавання - це процес встановлення відмітки для відомого обличчя. Аналогічно до того, як люди навчаються розпізнавати власну сім'ю, друзів та знаменитостей за їх обличчями, комп'ютер також може скористатися багатьма техніками навчання для цього. В загальному вони включають чотири основні кроки:

1. Пошук обличчя - це процес знаходження фрагмента обличчя на зображення (великий прямокутник поряд з центром наступного скріншота). На цьому кроці неважлива особа, а лише її обличчя.
2. Попередня обробка обличчя - це процес налаштування зображення обличчя, щоб воно виглядало більш чітким та схожим на інші (невеликий кадр у відтінках сірого зверху посередині малюнка).
3. Збір та вивчення даних - процес збереження багатьох попередньо оброблених облич (для кожної людини, яку маємо розпізнати) та подальші тренування алгоритму.
4. Розпізнавання обличчя - процес, що перевіряє, які із зібраних даних найбільше схожі на зображення, отримане з камери (малий прямокутник у правій верхній стороні ілюстрації).

Примітка
Словосполучення "розпізнавання обличчя" часто вживається у значенні пошуку його положення, але ця книга використовуватиме формальне визначення з четвертого етапу, а пошук посилатиметься на перший.

Далі проілюстровано готовий проєкт WebcamFaceRec, включно з невеликим прямокутником у верхньому правому куті, що виділяє розпізнану особу. Зверніть увагу на індикатор впевненості, що знаходиться поряд з попередньо обробленим обличчям (невеликий фрагмент посередині зверху), у даному випадку вірогідність коректного розпізнавання становить близько 70 відсотків.

Сучасні техніки пошуку обличчя достатньо надійні в умовах реального світу, а от розпізнавання похизуватися цим не може. Наприклад, легко знайти дослідження зі вказаною точністю розпізнавання обличчя більше 95 відсотків, але якщо протестувати ці алгоритми самостійно, їхня точність становитиме менше 50 відсотків.

Це походить з того факту, що сучасні техніки залежать від чітко встановлених вимог для кадрів, наприклад типу й напряму освітлення, тіней, точної орієнтації обличчя, виразу та настрою людини. Якщо ці умови постійні на етапі збору та тренування зображень, а також тестування на основі кадрів з камери, розпізнавання обличчя надає точні результати, але якщо людина стояла з лівого боку світла під час тренування, а під час тестування - з правого, результати дещо гірші. Тому етап формування набору даних дуже важливий.

Попередня обробка обличчя має зменшити кількість подібних проблем, відрегулювавши яскравість та контраст зображень і, можливо, встановивши однакові позиції для характеристик обличчя (вирівнявши очі та/або ніс). Якісно виконана процедура дозволяє покращити надійність усієї системи, тому пояснимо її детальніше.

Не зважаючи на суперечки щодо безпеки розпізнавання обличчя в медіапросторі, навряд чи сучасні методи достатньо надійні для реальної системи безпеки, але їх можна застосувати для цілей, які не потребують цього, наприклад програвання персоналізованої музики для різних людей, які входять у кімнату, чи робот, що звертається до вас за іменем. Існують також різні практичні розширення, як-от розпізнавання статі, віку та емоцій.

Етап 1. Пошук обличчя
До 2000-го року існувало багато технік для пошуку обличчя, але всі вони були або надто повільними, або дуже ненадійними. Важливі зміни відбулися у 2001 році, коли Віола та Джонс розробили заснований на алгоритмі Хаара каскадний класифікатор для пошуку об'єктів, та у 2002 році, коли його покращили Ліенхарт та Мейдіт. В результаті детектор виявився швидким (може розпізнавати обличчя в режимі реального часу на типовому ПК з вебкамерою) та надійним (коректно знаходить близько 95 відсотків облич у фронтальному ракурсі). Цей детектор спричинив революцію у сфері розпізнавання (робототехніці та комп'ютерному зорі) і врешті-решт дозволив створювати застосунки, що діють у режимі реального часу, особливо коли Ліенхарт заявив, що його детектор постачається вільно з бібліотекою OpenCV! Він здатний виявити не лише фронтальні фрагменти облич, а й профільні, очі, рот, ніс, логотипи компаній та багато інших об'єктів.

Детектор об'єктів розширено у версії 2.0 бібліотеки заради можливості використовувати характеристики LBP для пошуку, на основі роботи *Агонена, Хадіда та Пайтікейнена* у 2006 році, адже засновані на LBP детектори потенційно в декілька разів швидші за попередні алгоритми, а також не мають проблем з ліцензіями, притаманних багатьом детекторам Хаара. Базовою ідеєю детектора обличчя Хаара полягає в тому, що якщо ви поглянете на більшість фронтальних знімків облич, фрагмент з очима виглядатиме темнішим за лоб і щоки, а частина з ротом має бути темнішою за щоки, і так далі. Алгоритм типово здійснює понад 20 етапів подібних порівнянь для того, щоб класифікувати зображення як обличчя чи не-обличчя, але цей процес має бути здійснено у будь-якій можливій позиції та для будь-якого можливого розміру, тобто фактично необхідні тисячі перевірок. Основна ідея детектора LBP схожа на вище вказану, але для досягнення мети використовує гістограми порівняння інтенсивності пікселів, наприклад меж, кутів та плоских фрагментів.

Замість того, щоб найчіткіші порівняння для опису обличчя визначала людина, детектори Хаара та LBP можна натренувати автоматично знаходити потрібні дані з великого набору зображень та зберігати результати для пізнішого використання у вигляді файлів XML. Зазвичай, каскадні класифікатори тренують на основі щонайменше 1000 унікальних зображень обличчя та 10000 знімків інших об'єктів (наприклад, фотографій дерев, автомобілів та тексту), а тренування може зайняти довгий час навіть на багатоядерному процесорі (від декількох годин для LBP до тижня для Хаара!). На щастя, бібліотека OpenCV постачається з попередньо натренованити детекторами! Фактично, ви зможете шукати фронтальні та профільні фрагменти з обличчями, очі чи носи, лише завантаживши різні каскадні класифікатори з файлів XML в детектор об'єктів, а також обирати між детекторами Хаара чи LBP.

Реалізуємо розпізнавання обличчя з використанням OpenCV

Як вказано раніше, бібліотека OpenCV 2.4 постачається з різними попередньо натренованими детекторами XML, якими можна скористатися для різних цілей. У таблиці перелічено деякі з найпопулярніших файлів:
Тип каскадного класифікатора Файл XML
Детектор обличчя (за замовчуванням) haarcascade_frontalface_default.xml
Детектор обличчя (швидкий алг. Хаара) haarcascade_frontalface_alt2.xml
Детектор обличчя (швидкий алг. LBP) lbpcascade_frontalface.xml
Детектор профільних зображень обличчя haarcascade_profileface.xml
Детектор ока (окремо лівого й правого) haarcascade_lefteye_2splits.xml
Детектор рота haarcascade_mcs_mouth.xml
Детектор носа haarcascade_mcs_nose.xml
Детектор людини haarcascade_fullbody.xml

Засновані на алгоритмі Хаара детектори зберігаються у теці data\haarcascades, а LBP-детектори - за відносним шляхом data\lbpcascades у кореневій теці бібліотеки OpenCV, наприклад C:\opencv\data\lbpcascades\.

У нашому проєкті розпізнавання обличчя ми бажаємо знайти фронтальні фрагменти, тому застосуємо детектор LBP, адже він найшвидший та не потребує ліцензії. Враховуйте, заздалегідь натреноватий детектор LBP, що постачається з бібліотекою OpenCV v2.x не настільки добре відрегульований, як попередньо натреновані детектори облич Хаара, тому якщо вам потрібні більш надійні алгоритми, варто створити власний, на основі LBP, чи просто використати детектор Хаара.

Завантажуємо детектори Хаара або LBP для розпізнавання об'єкта чи обличчя

Щоб здійснити розпізнавання об'єкта чи обличчя, спершу потрібно завантажити заздалегідь натренований файл XML за допомогою класу CascadeClassifier таким чином:
CascadeClassifier faceDetector;
faceDetector.load(faceCascadeFilename);

Завдяки цьому можна завантажити детектори Хаара чи LBP, лише вказавши іншу назву файлу. Поширеною помилкою є передавання невірної теки чи назви файлу, але залежно від середовища побудови, метод load() завжди або поверне значення false, або згенерує виняток С++ (та завершить роботу програми помилкою assert). Тому краще оточити метод load() блоком try/catch та показати на екрані повідомлення про помилку. Початківці часто пропускають перевірку на помилки, забуваючи про важливість допоміжних повідомлень, коли щось відбулося не правильно. Але це варто робити, інакше ви витратите дуже багато часу на налагодження інших частин коду перед тим як дізнаєтесь, що файл не завантажено. Код простого повідомлення матиме такий вигляд:

CascadeClassifier faceDetector;
try {
faceDetector.load(faceCascadeFilename);
} catch (cv::Exception e) {}
if ( faceDetector.empty() ) {
cerr << "ПОМИЛКА: Не вдалося завантажити детектор обличчя (";
cerr << faceCascadeFilename << ")!" << endl;
exit(1);
}

Отримуємо доступ до вебкамери

Щоб отримати кадри з вебкамери комп'ютера чи навіть відеофайлу, досить просто викликати функцію VideoCapture::open(), передавши в якості аргументу номер камери чи назву відеофайла, потім здобути кадри за допомогою потокового оператора C++, як показано в частині "Отримуємо доступ до вебкамери" у розділі 1 "Ефект мультиплікації та зміна кольору для Android".

Знаходимо об'єкт за допомогою класифікатора Хаара чи LBP

Маючи завантажений класифікатор (у процесі ініціалізації), можемо застосувати його для пошуку облич на кожному новому кадрі камери. Але спершу маємо здійснити початкову обробки зображення, виконавши для цього такі кроки:

1. Перетворення у відтінки сірого. Розпізнавання обличчя працює лише з такими зображеннями. Ми повинні конвертувати кольорові кадри у відтінки сірого.
2. Зменшення розмірів кадру. Швидкість алгоритму залежить від розмірів вхідного зображення (дуже повільна для великих, але швидка для малих кадрів), а пошук дає пристойні результати навіть для низької розподільної здатності. Тому маємо зменшити зображення з камери до більш прийнятних розмірів (або використати велике значення minFeatureSize у детекторі).
3. Вирівнювання гістограми. Розпізнавання обличчя ненадійне в умовах слабкого освітлення. Тому потрібно здіснити цю процедуру для покращення контрасту та яскравості.

Перетворення у відтінки сірого

Кольорове зображення можна легко перетворити у відтінки сірого за допомогою функції cvtColor(). Але це потрібно робити лише тоді, коли ми маємо саме кольоровий знімок (камера не надає сірі зображення), і ми повинні визначити формат вхідного кадру (зазвичай 3-канальний BGR на настільному пристрої чи 4-канальний BGRA на мобільному). Тому варто підримувати три різні формати кольору, як показано в такому фрагменті коду:

Mat gray;
if (img.channels() == 3) {
cvtColor(img, gray, CV_BGR2GRAY);
}
else if (img.channels() == 4) {
cvtColor(img, gray, CV_BGRA2GRAY);
}
else {
// отримуємо доступ до зображення у відтінках сірого напряму
gray = img;
}

Зменшуємо розміри кадру камери

Для зменшення розмірів зображення можна скористатися функцією resize() або множником масштабування. Пошук обличчя зазвичай працює досить добре для кадрів з розмірами, що більші за 240х240 пікселів (якщо обличчя розміщені не занадто далеко від камери), адже алгоритм працюватиме для будь-яких із них, що більші за minFeatureSize (типово 20х20 пікселів). Тому зменшимо ширину отриманих кадрів до 320 пікселів; при цьому неважливо, вхідні дані отримані з вебкамери VGA чи 5-мегапіксельного HD пристрою. Не забудьте збільшити результати пошуку, адже знайдені обличчя також будуть зменшеними.

Замість зменшення розмірів вхідного зображення можна відрегулювати значення параметра minFeatureSize детектора. Фрагменти не мають стати ширшими або вужчими. Наприклад, зменшивши широке зображення з розмірами 800х400 до 300х200, отримаємо кадр, на якому людина видаватиметься худою. Тому потрібно зберегти відношення сторін (відношення ширини до висоти) вхідних даних. Обчислимо, наскільки потрібно зменшити ширину зображення, а потім застосуємо отриманий множник масштабування і для висоти:

const int DETECTION_WIDTH = 320;
// за можливості обмежимо розміри зображення, щоб збільшити швидкість алгоритму
Mat smallImg;
float scale = img.cols / (float) DETECTION_WIDTH;
if (img.cols > DETECTION_WIDTH) {
// зменшимо зображення, але залишимо відношення сторін
int scaledHeight = cvRound(img.rows / scale);
resize(img, smallImg, Size(DETECTION_WIDTH, scaledHeight));
}
else {
// отримаємо дані напряму, адже вони вже малі
smallImg = img;
}

Вирівнювання гістограми

Бібліотека OpenCV дозволяє легко здійснити вирівнювання гістограми для покращення контрасту та яскравості зображення за допомогою функції equalizeHist() (як пояснено в праці "Computer Vision with the OpenCV Library". Іноді це призводить до дивного вигляду зображення, але в загальному яскравість і контраст покращуються, і це допомагає швидше та надійніше знаходити обличчя. Функція equalizeHist() застосовується таким чином:

// стандартизуємо яскравість та контраст, щоб
// покращити темні зображення
Mat equalizedImg;
equalizeHist(inputImg, equalizedImg);

Пошук обличчя

Сконвертувавши зображення у відтінки сірого, зменшивши його розміри та вирівнявши гістограми, ми готові визначити обличчя за допомогою функції CascadeClassifier::detectMultiScale()! Вона має такі параметри:

minFeatureSize - визначає мінімальний розмір обличчя, типово становить 20х20 чи 30х30 пікселів, але залежить від конкретного випадку та розміру зображення. Якщо ви здійснюєте пошук обличчя на кадрі з вебкамери чи мобільного пристрою, коли воно знаходиться поряд із камерою, можна збільшити це значення до 80х80, щоб отримати результат швидше, а якщо потрібно знайти обличчя на відстані, наприклад серед осіб на пляжі, залиште 20х20.
searchScaleFactor - визначає кількість різних розмірів облич, які маємо знайти; типово становить 1.1 для якісного пошуку та 1.2 для швидкого, але не настільки надійного.

minNeighbors - визначає, наскільки впевненим має бути детектор, аби він міг знайти обличчя, звичайно становить 3, але можна збільшити це значення, щоб пошук формував надійніші результати, навіть якщо більшість облич не буде розпізнано.

flags - дозволяє визначити пошук усіх облич (за замовчуванням) або лише найбільшого (CASCADE_FIND_BIGGEST_OBJECT). Якщо потрібне лише найбільше, алгоритм знайде результат швидше. Існують також й інші параметри, які можуть пришвидшити процес, наприклад CASCADE_DO_ROUGH_SEARCH чи CASCADE_SCALE_IMAGE.

Результат функції detectMultiScale() матиме тип std::vector<cv::Rect>. Наприклад, якщо вона знайде два обличчя, в масиві буде збережено два прямокутники. Функція діє таким чином:

int flags = CASCADE_SCALE_IMAGE; // пошук багатьох облич
Size minFeatureSize(20, 20); // найменший розмір обличчя
float searchScaleFactor = 1.1f; // кількість розмірів для пошуку
int minNeighbors = 4; // надійність проти кількості
// шукаємо об'єкти на малому зображенні у відтінках сірого
std::vector<Rect> faces;
faceDetector.detectMultiScale(img, faces, searchScaleFactor,
minNeighbors, flags, minFeatureSize);

Щоб перевірити, чи були взагалі знайдені обличчя, можна оглянути кількість елементів, що зберігаються у векторі прямокутників, за допомогою функції objects.size().

Як вказано раніше, якщо ми передаємо зменшене зображення детектору обличчя, результати також будуть зменшені, тому необхідно зробити зворотні перетворення, якщо потрібні фрагменти обличчя на початковому кадрі. Варто також впевнитися у тому, що обличчя на границях зображення залишаються в його межах, оскільки у такому разі бібліотека OpenCV активує виключення:

// Збільшуємо результати, якщо зображення було тимчасово зменшене.
if (img.cols > scaledWidth) {
for (int i = 0; i < (int)objects.size(); i++ ) {
objects[i].x = cvRound(objects[i].x * scale);
objects[i].y = cvRound(objects[i].y * scale);
objects[i].width = cvRound(objects[i].width * scale);
objects[i].height = cvRound(objects[i].height * scale);
}
}
// Якщо об'єкт розміщений на границі, залишити його на зображенні.
for (int i = 0; i < (int)objects.size(); i++ ) {
if (objects[i].x < 0)
objects[i].x = 0;
if (objects[i].y < 0)
objects[i].y = 0;
if (objects[i].x + objects[i].width > img.cols)
objects[i].x = img.cols - objects[i].width;
if (objects[i].y + objects[i].height > img.rows)
objects[i].y = img.rows - objects[i].height;
}

Попередній код шукатиме всі обличчя на зображенні, але якщо вам потрібне якесь конкретне, змінну флагу можна вказати таким чином:
int flags = CASCADE_FIND_BIGGEST_OBJECT | CASCADE_DO_ROUGH_SEARCH;

Проєкт WebcamFaceRec включає клас-обгортку навколо детектора Хаара чи LBP, аби мати можливість легше знаходити обличчя чи око на зображенні. Наприклад:
Rect faceRect; // зберігає результати пошуку, або повертає -1
int scaledWidth = 320; // зменшуємо зображення перед пошуком
detectLargestObject(cameraImg, faceDetector, faceRect,
scaledWidth);
if (faceRect.width > 0)
cout << "Обличчя знайдено!" << endl;

Маючи обмежувальний прямокутник, можемо скористатися ним різними способами, наприклад для того, щоб вирізати фрагмент обличчя з оригінального зображення. Опишемо фрагмент коду, що дозволяє отримати доступ до цих даних:

// Виокремимо фрагмент обличчя із кадру камери.
Mat faceImg = cameraImg(faceRect);
Далі проілюстровано типовий прямокутний фрагмент, знайдений детектором обличчя:

Етап 2. Попередня обробка обличчя
Як раніше зазначено, процедура розпізнавання досить сильно залежить від умов освітлення, орієнтації та виразу обличчя і так далі, тому цю різницю потрібно мінімізувати якомога краще. Інакше алгоритм вважатиме, що обличчя двох різних людей в однакових умовах більш подібні між собою, аніж два обличчя тієї ж особи. Найлегшим способом попередньо обробити обличчя - просто застосувати функцію equalizeHist() для вирівнювання гістограми, як і було зроблено на етапі пошуку обличчя.

Етап 2. Попередня обробка обличчя
Як раніше зазначено, процедура розпізнавання досить сильно залежить від умов освітлення, орієнтації та виразу обличчя і так далі, тому цю різницю потрібно мінімізувати якомога краще. Інакше алгоритм вважатиме, що обличчя двох різних людей в однакових умовах більш подібні між собою, аніж два обличчя тієї ж особи. Найлегшим способом попередньо обробити обличчя - просто застосувати функцію equalizeHist() для вирівнювання гістограми, як і було зроблено на етапі пошуку обличчя.

Пошук ока

Пошук ока може виявитися корисним етапом попередньої обробки обличчя, тому що для фронтальних зображень облич можемо припустити, що очі людини розміщені горизонтально та симетрично відносно середини та повинні мати достатньо стандартні позиції та розміри відповідних фрагментів на зображенні, не зважаючи на зміни виразів обличчя, умови освітлення, властивості камери, відстань до неї і так далі. Також корисно відкидати хибні появи, коли детектор знаходить обличчя, але насправді отриманий об'єкт ним не є. Дуже рідко детектор обличчя та два детектори ока, діючи одночасно, формують неправильні результати, тому якщо ви обробляєте лише зображення з обличчям та двома очами, хибних появ буде не так багато (але в цьому разі матимемо менше об'єктів для обробки, бо детектор ока знаходить результат не так часто, як детектор обличчя).

Деякі із заздалегідь натренованих детекторів, що постачаються разом з бібліотекою OpenCV v2.4, можуть розпізнавати і відкриті, і закриті очі, у той час як інші - тільки закриті.

Детектори першого типу зберігаються у таких файлах:
haarcascade_mcs_lefteye.xml та haarcascade_mcs_righteye.xml
haarcascade_lefteye_2splits.xml та haarcascade_righteye_2splits.xml
Лише одне око розпізнають такі детектори:
haarcascade_eye.xml
haarcascade_eye_tree_eyeglasses.xml

Примітка
Оскільки процес розпізнавання відкритого чи закритого стану визначає, яке саме око використовується для тренування, потрібні окремі детектори для лівого та правого. Що ж до детекторів винятково для відкритого ока, їх об'єкт для розпізнавання залишається однаковим для обох.

Детектор haarcascade_eye_tree_eyeglasses.xml здатний розпізнати очі, якщо людина носить окуляри, але не дуже надійний, якщо це не так.
Якщо назва файлу XML включає словосполучення "ліве око", це значить саме ліве око людини, а не будь-яке інше. Тобто на кадрі з камери воно знаходитиметься з правого боку обличчя, а не лівого! Список чотирьох зазначених детекторів ока упорядкований від найбільш надійних до найменш. Якщо вам не потрібно розпізнавати людей в окулярах, перший варіант надаватиме найкращі результати.

Зони пошуку ока

У процесі розпізнавання важливим кроком є виокремлення фрагмента з приблизним положенням ока, для цього потрібно відшукати обличчя та вирізати невеликий прямокутник з лівим або правим оком залежно від застосованого детектора. Якщо ви виконаєте розпізнавання ока на усьому обличчі чи фотографії, страждатиме швидкість та надійність. Різні детектори ока краще працюють для певних фрагментів обличчя, наприклад haarcascade_eye.xml формує якісніші результати для дуже малих прямокутників навколо самого ока, а детектори haarcascade_mcs_lefteye.xml та haarcascade_lefteye_2splits.xml діють краще для великих фрагментів.

У таблиці перелічено непогані регіони пошуку обличчя для різних детекторів ока (з використанням алгоритму розпізнавання LBP), з такими відносними координатами у межах знайденого прямокутника:
Каскадний класифікатор EYE_SX EYE_SY EYE_SW EYE_SH
haarcascade_eye.xml 0.16 0.26 0.30 0.28
haarcascade_mcs_lefteye.xml 0.10 0.19 0.40 0.36
haarcascade_lefteye_2splits.xml 0.12 0.17 0.37 0.36

Далі вкажемо код для вирізання фрагментів з лівим та правим оком зі знайденого обличчя:
int leftX = cvRound(face.cols * EYE_SX);
int topY = cvRound(face.rows * EYE_SY);
int widthX = cvRound(face.cols * EYE_SW);
int heightY = cvRound(face.rows * EYE_SH);
int rightX = cvRound(face.cols * (1.0-EYE_SX-EYE_SW));
Mat topLeftOfFace = faceImg(Rect(leftX, topY, widthX,
heightY));
Mat topRightOfFace = faceImg(Rect(rightX, topY, widthX,
heightY));

Наступний малюнок зображає ідеальні регіони пошуку для різних детекторів ока. Бачимо, що haarcascade_eye.xml та haarcascade_eye_tree_eyeglasses.xml краще підходять для невеликих, а haarcascade_mcs_*eye.xml та haarcascade_*eye_2splits.xml - для великих. Також показано знайдений прямокутник обличчя, щоб проілюструвати, наскільки великі зони пошуку ока порівняно з ним.

Ось приблизні значення параметрів розпізнавання для різних детекторів ока:

Каскадний класифікатор Надійність* Швидкість** Тип знайденого ока Діє для окулярів
haarcascade_mcs_lefteye.xml 80% 18мс відкрите або закрите ні
haarcascade_lefteye_2splits.xml 60% 7мс відкрите або закрите ні
haarcascade_eye.xml 40% 5мс лише відкрите ні
haarcascade_eye_tree_eyeglasses.xml 15% 10мс лише відкрите так

* Значення надійності вказує на те, наскільки часто обидва ока будуть знайдені за допомогою алгоритму розпізнавання фронтального обличчя LBP, якщо особа не носить окуляри, а очі відкриті. Якщо ж вони закриті, або окуляри мають певні дефекти, надійність і швидкість може знизитися.

** Значення швидкості вказані в мілісекундах для зображень, масштабованих до розмірів 320х240 пікселів на процесі Intel Core i7 2.2 GHz (середнє серед тисячі фотографій). Зазвичай швидкість більша, коли очі знаходяться, ніж у протилежному випадку, бо для цього може знадобитися просканувати все зображення, але навіть у такому разі детектор haarcascade_mcs_lefteye.xml все ще повільніший за інші.

Наприклад, ви маєте зменшити розміри фотографії до 320х240 пікселів, здійснити вирівнювання гістограми, скористатися детектором фронтальних облич LBP, виокремити регіони лівого та правого ока за допомогою значень haarcascade_mcs_lefteye.xml, потім виконати вирівнювання гістограми для кожного з них. Якщо використовуєте детектор haarcascade_mcs_lefteye.xml для лівого ока (у верхній правій частині зображення) та haarcascade_mcs_righteye.xml для правого (у верхньому лівому куті), кожен детектор ока має діяти для 90 відсотків фотографій зі знайденими фронтальними обличчями LBP. Тому, якщо потрібно розпізнати обидва ока, складений алгоритм повертатиме результат для близько 80 відсотків зображень знайдених облич.

Хоча й рекомендовано вирізати відповідний фрагмент кадру камери перед пошуком обличчя, очі потрібно розпізнавати на оригінальному зображенні. Вочевидь, вони значно менші за обличчя, тому роздільна здатність має бути якомога більшою.

Примітка
Згідно таблиці, обирати детектор потрібно залежно від того, чи бажаєте ви розпізнавати закриті очі, чи лише відкриті. Пам'ятайте, що можна скористатися одним алгоритмом, а якщо він не поверне прийнятні результати, спробувати інший. Для багатьох задач важливо розпізнати, відкриті очі чи закриті, тому якщо швидкість роботи програми неважлива, спочатку варто здійснити пошук за допомогою детектора msc_*eye, а якщо результати не вдасться знайти, спробувати eye_2splits.

Але у процесі розпізнавання обличчя людина видаватиметься дещо іншою, якщо її очі закриті, тому спочатку варто скористатися простим детектором haarcascade_eye, а потім - здійснити пошук за допомогою haarcascade_eye_tree_eyeglasses.

Можна скористатися тією ж функцією detectLargestObject(), що й на етапі розпізнавання обличчя для пошуку очей, але замість вирізання фрагментів зображень перед етапом пошуку визначаємо ширину регіону повного ока, щоб отримати кращі результати для процесу. Ліве око легко знайти за допомогою одного детектора, а якщо він не поверне результати, можна спробувати інший (той же процес повторимо й для правого). Розпізнавання ока реалізоване таким чином:

CascadeClassifier eyeDetector1("haarcascade_eye.xml");
CascadeClassifier
eyeDetector2("haarcascade_eye_tree_eyeglasses.xml");
...
Rect leftEyeRect; // зберігаємо розпізнане око
// знайдемо лівий фрагмент за допомогою детектора першого ока
detectLargestObject(topLeftOfFace, eyeDetector1, leftEyeRect,
topLeftOfFace.cols);
// якщо результатів немає, спробуємо другий детектор
if (leftEyeRect.width <= 0)
detectLargestObject(topLeftOfFace, eyeDetector2,
leftEyeRect, topLeftOfFace.cols);
// знаходимо центр лівого ока, якщо хоча б один з детекторів спрацював
Point leftEye = Point(-1,-1);
if (leftEyeRect.width <= 0) {
leftEye.x = leftEyeRect.x + leftEyeRect.width/2 + leftX;
leftEye.y = leftEyeRect.y + leftEyeRect.height/2 + topY;
}
// робимо те ж для правого ока
...
// перевіряємо, чи були розпізнані обидва ока
if (leftEye.x >= 0 && rightEye.x >= 0) {
...
}

Маючи розпізнані обличчя та очі, здійснимо такі етапи попередньої обробки зображення:
Геометричні перетворення та вирізання. Цей процес включає масштабування, поворот та перенесення зображень з метою вирівнювання очей та видалення лоба, щік, вух і фону зображень.

Окреме вирівнювання гістограм для лівого та правого боків. На цьому етапі стандартизуємо яскравість та контраст для обох частин зображення обличчя незалежно одна від одної.
Згладжування. Видаляє шум за допомогою двопрохідного фільтра.

Еліптична маска. Видаляє волосся та фон із зображення.

Наступний малюнок ілюструє етапи 1-4 попередньої обробки знайденого фрагмента обличчя. Результат має однакові показники яскравості та контрасту на обох боках обличчя, у той час як оригінальні зображення - ні.

Геометричні перетворення

Важливо мати вирівнені між собою обличчя, інакше алгоритм розпізнавання може порівнювати частину носа з оком тощо. Показаний результат має вирівнені обличчя, але не досить точні (тобто прямокутник не завжди починається у тій же точці лоба).

Щоб отримати кращі результати для вирівнювання, розпізнаємо очі та розмістимо їх чітко у бажаних позиціях. Геометричні перетворення здійснимо за допомогою функції warpAffine(), простої операції, яка робить чотири речі:
Повертає обличчя, щоб отримати горизонтальні положення очей.

Масштабує обличчя, щоб відстань між очима не змінювалася.
Переносить обличчя, щоб очі були горизонтально відцентровані та мали бажану висоту.
Видаляє верхні частини обличчя, оскільки нам не потрібні фон та волосся, лоб, вуза та щоки.

Афінні викривлення діють на матрицю, що переносить положення двох знайдених очей у бажані позиції, а потім виконує певні операції, щоб отримати бажані розміри та позицію. Щоб згенерувати афінну матрицю, знайдемо центральну точку між очима, обчислимо кут та відстань між ними:

// знайдемо центральну точку між очима
Point2f eyesCenter;
eyesCenter.x = (leftEye.x + rightEye.x) * 0.5f;
eyesCenter.y = (leftEye.y + rightEye.y) * 0.5f;
// обчислимо кут між очима
double dy = (rightEye.y - leftEye.y);
double dx = (rightEye.x - leftEye.x);
double len = sqrt(dx*dx + dy*dy);
// конвертуємо радіани в градуси
double angle = atan2(dy, dx) * 180.0/CV_PI;
// ручні виміри для центра лівого ока в ідеальному випадку
// мають становити приблизно (0.16, 0.14) для масштабованого зображення обличчя
const double DESIRED_LEFT_EYE_X = 0.16;
const double DESIRED_RIGHT_EYE_X = (1.0f – 0.16);

// встановлюємо фіксовані бажані розміри обличчя
const int DESIRED_FACE_WIDTH = 70;
const int DESIRED_FACE_HEIGHT = 70;
double desiredLen = (DESIRED_RIGHT_EYE_X – 0.16);
double scale = desiredLen * DESIRED_FACE_WIDTH / len;
Тепер можемо здійснити перетворення (поворот, масштабування та перенесення), щоб отримати два ока у бажаних позиціях ідеального обличчя таким чином:
// отримуємо матрицю перетворень для бажаних кута та розмірів
Mat rot_mat = getRotationMatrix2D(eyesCenter, angle, scale);

// зміщуємо центральні точки очей у бажаний центр
double ex = DESIRED_FACE_WIDTH * 0.5f - eyesCenter.x;
double ey = DESIRED_FACE_HEIGHT * DESIRED_LEFT_EYE_Y –
eyesCenter.y;
rot_mat.at<double>(0, 2) += ex;
rot_mat.at<double>(1, 2) += ey;
// перетворюємо зображення обличчя з урахуванням бажаних
// кута, розмірів та позиції; також встановлюємо сірий
// колір фону
Mat warped = Mat(DESIRED_FACE_HEIGHT, DESIRED_FACE_WIDTH,
CV_8U, Scalar(128));
warpAffine(gray, warped, rot_mat, warped.size());

Окремі вирівнювання гістограм для лівого та правого боків

В реальних умовах часто один бік обличчя освітлений сильніше за інший. Це має величезний вплив на алгоритм розпізнавання обличчя, оскільки лівий та правий бік того ж обличчя видаватимуться як належні різним людям. Тому здійснимо вирівнювання гістограми окремо для лівої та правої половини обличчя, щоб стандартизувати яскравість та контраст для кожного боку.

Якщо ми просто застосуємо вирівнювання гістограми для лівого боку, а потім знову для правого, сформується чітка межа, бо середня яскравість очевидно відрізнятиметься для обох частин. Щоб її видалити, вирівнювання гістограми виконуватимемо поступово з лівого чи правого боків до центра та змішаємо їх з повною гістограмою обличчя, щоб правий бік використовував вирівнену праву гістограму, а центр - згладжену суміш лівого, правого та значення для гістограми повного обличчя.

Наступний малюнок ілюструє, як змішати між собою зображення з відрегульованими значеннями зліва, справа та повного кадру:

Для цього потрібні копії відрегульованих фрагментів зображення, які можна отримати таким чином:
int w = faceImg.cols;
int h = faceImg.rows;
Mat wholeFace;
equalizeHist(faceImg, wholeFace);
int midX = w/2;
Mat leftSide = faceImg(Rect(0,0, midX,h));
Mat rightSide = faceImg(Rect(midX,0, w-midX,h));
equalizeHist(leftSide, leftSide);
equalizeHist(rightSide, rightSide);

Тепер комбінуємо три зображення. Оскільки вони малі, можна легко отримати доступ до пікселів напряму за допомогою функції image.at<uchar>(y,x), навіть якщо вона повільна; тому поєднаємо три кадри, сформувавши вихідні пікселі з відповідників на цих зображеннях:

for (int y=0; y<h; y++) {
for (int x=0; x<w; x++) {
int v;
if (x < w/4) {
// 25% зліва з краю: використаємо лише ліву частину обличчя
v = leftSide.at<uchar>(y,x);
}
else if (x < w*2/4) {
// 25% зліва від центру: змішаємо ліву частину та все обличчя
int lv = leftSide.at<uchar>(y,x);
int wv = wholeFace.at<uchar>(y,x);
// додаватимемо більшу пропорцію цілого обличчя, чим
// правіше рухатимося вздовж нього
float f = (x - w*1/4) / (float)(w/4);
v = cvRound((1.0f - f) * lv + (f) * wv);
}
else if (x < w*3/4) {
// 25% справа від центру: змішаємо праву частину та все обличчя
int rv = rightSide.at<uchar>(y,x-midX);
int wv = wholeFace.at<uchar>(y,x);
// додаватимемо більшу пропорцію правої частини обличчя, чим
// правіше рухатимося вздовж нього
float f = (x - w*2/4) / (float)(w/4);
v = cvRound((1.0f - f) * wv + (f) * rv);
}
else {
// 25% справа з краю: використаємо лише праву частину обличчя
v = rightSide.at<uchar>(y,x-midX);
}
faceImg.at<uchar>(y,x) = v;
}// кінець циклу x
}// кінець циклу y

Подібна процедура окремого вирівнення гістограм повинна значно знизити ефект різного освітлення з лівого та правого боків обличчя, але не повністю, оскільки обличчя - це складна тривимірна форма з багатьма тінями.

Згладжування

Щоб зменшити ефект піксельного шуму, застосуємо двопрохідний фільтр, адже він досить добре згладжує зображення, залишаючи межі гострими. Вирівнювання гістограми може суттєво підвищити зашумленість, тому підвищимо силу фільтру до 20, щоб видалити шум, але встановимо значення два в якості кількості сусідів, оскільки ми бажаємо згладити лише крихітні острівці піксельного шуму, а не величезні регіони зображень:

Mat filtered = Mat(warped.size(), CV_8U);
bilateralFilter(warped, filtered, 0, 20.0, 2.0);

Еліптична маска

Хоча ми вже видалили більшу частину фону зображення, лоб та волосся, коли здійснювали геометричні перетворення, варто скористатися еліптичною маскою, щоб запобігти появі кутових фрагментів, наприклад шиї з тінню від обличчя, особливо якщо погляд не спрямований чітко на камеру. Щоб створити маску, намалюємо заповнений чорним кольором еліпс на білому фоні. Вона може мати горизонтальний радіус 0.5 (ідеально покриває ширину обличчя) і вертикальний 0.8 (оскільки обличчя звичайно мають більшу висоту), та мати центральну точку з координатами (0.5, 0.4). Далі показано, як за допомогою еліптичної маски видалено небажані кути обличчя:

Скористатися маскою можна за допомогою функції cv::setTo(), яка зазвичай встановлює всі пікселі зображення у певне значення, але якщо на вхід подати маску, відповідний показник встановиться лише для певних його частин.

// малюємо заповнений чорним кольором еліпс посередині зображення
// спочатку встановлюємо білий колір фону маски
Mat mask = Mat(warped.size(), CV_8UC1, Scalar(255));
double dw = DESIRED_FACE_WIDTH;
double dh = DESIRED_FACE_HEIGHT;
Point faceCenter = Point( cvRound(dw * 0.5),
cvRound(dh * 0.4) );
Size size = Size( cvRound(dw * 0.5), cvRound(dh * 0.8) );
ellipse(mask, faceCenter, size, 0, 0, 360, Scalar(0),
CV_FILLED);
// застосуємо еліптичну маску до обличчя, щоб видалити кути
// встановлюємо сірий колір для кутів, не дотикаючись до внутрішній частин
filtered.setTo(Scalar(128), mask);

Збільшений малюнок показує приклад результату виконання усіх етапів попередньої обробки обличчя. Ця процедура більш стійка до зміни яскравості, повороту, напрямку камери, фону, позицій світла і так далі. Попередньо оброблене обличчя використаємо як вхідні дані для подальших етапів, збору облич для тренування та власне розпізнавання:

Етап 3: Збір облич та тренування

Збирати обличчя настільки ж просто, як і розміщувати кожне новооброблене обличчя в масив попередньо опрацьованих кадрів камери, а також зберігати відмітку в масиві (щоб мати можливість визначати, кому саме належить обличчя). Наприклад, можна використати 10 заздалегідь оброблених облич першої особи та 10 другої, щоб вхідні дані алгоритму розпізнавання обличчя включали масив 20 попередньо опрацьованих облич та масив 20 цілих чисел (серед яких перші десять - нулі, а наступні - одиниці).

Далі алгоритм розпізнавання навчатиметься розрізняти обличчя різних людей. Цей етап називають тренуванням, а зібрані дані - тренувальним набором. Після його завершення, можна зберегти здобуті знання у пам'яті або файлі для пізнішого використання, щоб упізнати, хто саме зображений на знімку. Цей етап називають тестуванням. Якщо вхідні дані подано напряму з камери, то заздалегідь оброблене обличчя називатимемо тестовим зображенням, а якщо тестування відбувається за допомогою багатьох кадрів (наприклад, цілої теки), такі дані називатимемо тестовим набором.

Важливо забезпечити якісний тренувальний набір, який покриє всі типи очікуваних змін у тестовому наборі. Наприклад, якщо тестуватимемо лише обличчя осіб, погляд яких спрямований чітко на камеру (фотографії паспортів), знадобляться тренувальні зображення саме таких облич. Але якщо особа дивиться вліво чи вгору, набір має включати і такі кадри, інакше алгоритм розпізнавання обличчя навряд чи поверне результати, оскільки у цьому випадку вони матимуть дещо інший вигляд.

Це також стосується інших факторів, наприклад виразу обличчя (якщо людина завжди посміхається на кадрах тренувального набору, але не посміхається на тестових зображеннях) чи напряму освітлення (сильне світло з лівого боку на тренувальних фрагментах і з правого боку на тестових), у такому разі алгоритму складніше здійснити розпізнавання. Розглянутий раніше етап попередньої обробки зображення зменшить подібні проблеми, але навряд чи анулює вплив цих факторів, особливо в напрямі погляду, оскільки він має значний вплив на розміщення усіх елементів обличчя.

Примітка

Щоб отримати якісний тренувальний набір, що покриє різні реальні умови для кожної особи, можна скористатися знімками обличчя з поглядом, чітко спрямованим вліво, вверх, вправо та вниз. Потім людина нахиляє голову в боки, вгору та вниз, а також дещо змінює вираз обличчя, наприклад посміхається, виглядає розлюченою чи спокійною. Якщо кожна особа здійснить таку ж процедуру під час збору даних, вірогідність розпізнати усіх в реальних умовах значно підвищиться.

Для навіть кращих результатів процес варто здійснити ще для одного чи двох положень чи напрямків, наприклад здійснивши поворот камери на 180 градусів та повторивши процедуру зі зворотнього боку камери, щоб тренувальний набір включав знімки з різними умовами освітлення.

У загальному випадку, 100 тренувальних знімків облич для кожної людини дасть кращі результати, аніж 10, але якщо всі 100 облич виглядають практично однаковими, результат все ще буде поганим, адже більш важливим є те, щоб тренувальний набір мав достатню варіативність, щоб мати змогу покрити потрібні тестові умови, а не просто велику кількість різних облич. Щоб впевнитися, що не всі обличчя у тренувальному наборі схожі, маємо додати помітну затримку між кожним знімком. Наприклад, якщо камера діє зі швидкість 30 кадрів у секунду, вона здатна зібрати 100 облич за декілька секунд, а людина не матиме можливості повернутися за цей час, тому краще зберігати лише один знімок в секунду, даючи особі час для руху. Ще одним простим методом покращити варіативність тренувального набору - збирати обличчя, які не схожі на попередні.

Збираємо попередньо оброблені обличчя для тренування

Щоб впевнитися, що між знімками нових облич існує проміжок принаймні в одну секунду, потрібно вимірювати час. Це робиться таким чином:
// перевіримо, скільки часу пройшло з моменту додавання попереднього обличчя
double current_time = (double)getTickCount();
double timeDiff_seconds = (current_time –
old_time) / getTickFrequency();

Щоб порівняти попіксельну подібність двох кадрів, можна обчислити відносну похибку L2, тобто відняти одне зображення від іншого, просумувати квадрати значень та знайти корінь квадратний від отриманого результату. Якщо людина не здійснила жодних рухів, результат віднімання поточного та попереднього облич має досить низькі значення в кожному пікселі, яле якщо відбувся рух у будь-якому напряму, результат віднімає сформує досить великі числа, тобто похибка L2 буде високою. Оскільки результат сумується по всім пікселям, значення сильно залежить від роздільної здатності зображення. Тому, щоб знайти середню похибку, поділимо це значення на загальну кількість пікселів зображення. Для цього скористаємось зручною функцією getSimilarity():

double getSimilarity(const Mat A, const Mat B) {
// обчислимо відносну похибку L2 між двома зображеннями
double errorL2 = norm(A, B, CV_L2);
// масштабуємо значення, оскільки L2 сумується по всім пікселям
double similarity = errorL2 / (double)(A.rows * A.cols);
return similarity;
}
...
// перевіряємо, чи обличчя виглядає інакше, ніж попереднє
double imageDiff = MAX_DBL;
if (old_prepreprocessedFaceprepreprocessedFace.data) {
imageDiff = getSimilarity(preprocessedFace,
old_prepreprocessedFace);
}

Значення схожості становитиме менше 0.2, якщо зображення не дуже змістилося, та вище 0.4, якщо відбувся значний рух, тому скористуємось значенням 0.3 в якості порогу для збору нового обличчя. Існує багато трюків, які можна застосувати для того, щоб отримати більше тренувальних даних, наприклад віддзеркалення, випадковий шум, зміщення обличчя на декілька пікселів, масштабування на певний відсоток чи поворот на декілька градусів (навіть якщо ми намагалися видалити ці ефекти на етапі попередньої обробки). Додамо віддзеркалені обличчя до тренувального набору, щоб збільшити розміри і знизити похибки асиметричних облич чи постійної орієнтації користувача в лівий чи правий бік під час тренування, але не тестування. Це можна зробити таким чином:

// Оброблювати обличчя, якщо ті помітно відрізняються
// від попереднього кадру, і пройшов прийнятний проміжок часу.
if ((imageDiff > 0.3) && (timeDiff_seconds > 1.0)) {
// Додаємо віддзеркалене зображення до тренувального набору.
Mat mirroredFace;
flip(preprocessedFace, mirroredFace, 1);
// Додаємо звичайне і віддзеркалене обличчя до списку знайдених об'єктів.
preprocessedFaces.push_back(preprocessedFace);
preprocessedFaces.push_back(mirroredFace);
faceLabels.push_back(m_selectedPerson);
faceLabels.push_back(m_selectedPerson);
// Зберігаємо копію оброблених облич,
// щоб порівняти їх на наступній ітерації.
old_prepreprocessedFace = preprocessedFace;
old_time = current_time;
}

Цей фрагмент коду наповнить вектори preprocessedFaces та faceLabels попередньо обробленими обличчями, а також відмітками чи номером паспорта цієї особи (припускаючи, що він зберігається у змінній m_selectedPerson).

Щоб оповістити користувача про те, що його обличчя додано до колекції, достатньо вивести великий білий прямокутник на поверхні зображення або просто показати кадр на долі секунди. Завдяки С++ інтерфейсу бібліотеки OpenCV можна скористатися перевантаженим оператором + об'єкта cv::Mat, щоб додати значення кожного пікселя до зображення та обмежити його на рівні 255 (за допомогою функції saturate_cost, щоб білий колір не перетворився в чорний!). Припустимо, що displayedFrame є копією кольорового кадру камери та додамо такий фрагмент коду до процедури збору облич:

// Отримаємо доступ до регіону інтересу обличчя.
Mat displayedFaceRegion = displayedFrame(faceRect);
// Зробимо пікселі регіону дещо яскравішими.
displayedFaceRegion += CV_RGB(90,90,90);

Тренування системи розпізнавання на основі колекції облич

Отримавши достатню колекцію облич кожної особи, потрібно натренувати систему за допомогою алгоритму машинного навчання, що підходить для розпізнавання. В літературі описано багато різних алгоритмів, найпростішими серед яких є методи Eigenfaces та штучні нейромережі. Перший спосіб працює краще і, не зважаючи на простоту, діє настільки ж добре, як і більш складні алгоритми розпізнавання обличчя, тому його часто використовують як фундамент для початківців та побудови нових процедур.

Якщо ви бажаєте й надалі вивчати питання розпізнавання обличчя, перегляньте теоретичні відомості, що стосуються таких тем:
Eigenfaces (аналіз головних компонентів, Principal Component Analysis, PCA)
Fisherfaces (аналіз лінійного дискримінанта, Linear Discriminant Analysis, LDA)
Інші класичні алгоритми розпізнавання (доступні за адресою http://www.face-
rec.org/algorithms/)

Більш нові алгоритми з нещодавно викладених у публічний доступ дослідницьних праць (CVPR чи ICCV в http://www.cvpapers.com/), оскільки кожного року публікуються сотні подібних статей.

Але вам не обов'язково знати теоретичні викладки алгоритмів, щоб користуватися ними для поставлених у книзі завдань. Дякуючи команді OpenCV та вкладу Філіпа Вагнера в libfacerec, версія 2.4.1 постачає простий та загальний метод cv::Algorithm для здійснення розпізнавання обличчя за допомогою одного з декількох різних алгоритмів (які навіть можна обирати в режимі реального часу) без розуміння деталей їх реалізації. Щоб переглянути доступні алгоритми для вашої версії бібліотеки OpenCV, досить викликати функцію Algorithm::getList()

vector<string> algorithms;
Algorithm::getList(algorithms);
cout << "Доступні алгоритми: " << algorithms.size() << endl;
for (int i=0; i<algorithms.size(); i++) {
cout << algorithms[i] << endl;

У бібліотеці OpenCV v2.4.1 доступні три алгоритми:
FaceRecognizer.Eigenfaces. Також відомі як PCA, вперше застосовані Тюрком та Пентлендом в 1991 році.
FaceRecognizer.Fisherfaces. Також відомі як LDA, винайдені Белхьюмером, Хеспаном та Крігменом у 1997 році.
FaceRecognizer.LBPH. Гістограми локальних бінарних шаблонів, розроблені Еоненом, Хедідом та Пайтікейненом у 2004 році.

Примітка
Більше даних про ці реалізації алгоритму розпізнавання обличчя можна знайти в документації, прикладах та фрагментах коду Python на вебсайті Філіпа Вагнера за адресами http://bytefish.de/blog та http://bytefish.de/dev/libfacerec/.

Ці алгоритми розпізнавання обличчя доступні в класі FaceRecognizer модуля contrib бібліотеки OpenCV. Зважаючи на динамічне лінкування, можлива ситуація, за якої програму поєднано з модулем, але він не завантажений (якщо відповідний файл не знайдено). Тому рекомендовано викликати функцію cv::initModule_contrib() до спроб отримати доступ до алгоритмів FaceRecognizer. Така функція доступна в бібліотеці OpenCV, починаючи з версії 2.4.1, вона дозволяє впевнитися, що алгоритми розпізнавання доступні на етапі компіляції.

// Завантажимо модуль contrib динамічно
bool haveContribModule = initModule_contrib();
if (!haveContribModule) {
cerr << "ПОМИЛКА: модуль contrib, необхідний для ";
cerr << "FaceRecognizer, не вдалося завантажити!";
cerr << endl;
exit(1);
}

Щоб скористатися одним з алгоритмів розпізнавання обличчя, потрібно створити об'єкт FaceRecognizer за допомогою функції cv::Algorithm::create<FaceRecognizer>(). В якості параметра передаємо назву бажаного алгоритму у вигляді стрічки. Це дасть змогу застосовувати його, якщо він доступний у нашій версії. Також можна використати цей факт для перевірки, має користувач бібліотеку OpenCV v2.4.1 і вище чи ні. Наприклад:

string facerecAlgorithm = "FaceRecognizer.Fisherfaces";
Ptr<FaceRecognizer> model;
// Скористаємось новим класом FaceRecognizer з модуля contrib.
model = Algorithm::create<FaceRecognizer>(facerecAlgorithm);
if (model.empty()) {
cerr << "ПОМИЛКА: Алгоритм [[" << facerecAlgorithm;
cerr << "] недоступний для вашої версії OpenCV.";
cerr << "Будь-ласка, оновіть OpenCV до версії 2.4.1 чи старшої." << endl;
exit(1);
}

Завантаживши алгоритм FaceRecognizer, можна просто викликати функцію FaceRecognizer::train() із зібраними даними таким чином:
// Здійснюємо попереднє тренування на основі колекції облич
model->train(preprocessedFaces, faceLabels);

Ця стрічка коду розпочинає процес тренування обраного алгоритму (наприклад, Eigenfaces, Fisherfaces або будь-який інший). Якщо є лише декілька людей і менше за 20 облич, тренування займе незначний проміжок часу, але для багатьох людей з великою кількістю знімків, для обробки даних може знадобитися декілька секунд чи навіть хвилин.

Огляд отриманих знань

Хоч це й не обов'язково, згенеровані алгоритмом розпізнавання обличчя внутрішні структури даних корисно переглядати, особливо якщо ви розумієте теорію та бажаєте перевірити коректність результати чи знайти причини похибок. Внутрішні структури даних можуть відрізнятися від алгоритму до алгоритму, але, на щастя, вони однакові для Eigenfaces та Fisherfaces, тому розглянемо саме їх. Обидві представлені у вигляду одновимірних матриць власних векторів, схожих на обличчя, якщо переглядати їх як двовимірні зображення, тому для алгоритму Eigenfaces їх ще називають власними обличчями, а для Fisherfaces - обличчями Фішера.

Простими словами, суть методу Eigenfaces полягає в пошуку набору особливих зображень (власних облич) та відношень змішування (власних значень), які можна комбінувати різними способами для створення зображень тренувального набору та формування відмінностей. Наприклад, якщо деякі обличчя мають вуса, а деякі ні, матимемо як мінімум одне власне обличчя з вусами, тому такі тренувальні зображення матимуть високі показники змішування з власним вектором, а кадри без вус - низькі.

Якщо набір сформовано для 5 осіб з 20-ма фрагментами облич для кожної, для розпізнавання 100 облич знадобляться 100 власних облич та 100 власних значень, причому вони відсортовані таким чином, щоб декілька перших елементів вказували на найбільш значимі відмінності, а останні - всього лише випадкові шуми, які ніяк не допомагають розрізнити дані. Прийнято відкидати останні власні обличчя і залишати приблизно 50 елементів.

На противагу цьому, головний принцип методу Fisherfaces замість обчислення особливого власного вектора та власного значення для кожного зображення тренувального набору вимагає пошуку одного вектора та одного значення для особи. У попередньому прикладі 5 людей та 20 знімків облич для кожної, алгоритм Eigenfaces скористається сотнею власних облич та власних значень, у той час як метод Fisherfaces - лише п'ятьма.

Щоб отримати доступ до внутрішніх структур даних алгоритмів Eigenfaces та Fisherfaces, ми повинні скористатися функцією cv::Algorithm::get() у процесі роботи програми, оскільки вона не діє на етапі компіляції. Ці структури використовуються для математичних обчислень, а не обробки зображень, тому зберігаються у вигляді плаваючих чисел з типовим діапазоном від 0.0 до 1.0, а не 8-бітних пікселів uchar від 0 до 255, подібно до звичайних зображень.

Крім цього, це або одновимірний вектор чи один з багатьох рядів чи колонок більшої матриці. Перед тим як показати внутрішні структури даних, потрібно надати їм коректної прямокутної форми та конвертувати у 8-бітні пікселів uchar, що мають значення від 0 до 255. Оскільки дані матриці можуть належати діапазону від 0.0 до 1.0 чи від -1.0 до 1.0, можна скористатися функцією cv::normalize() з опцією cv::NORM_MINMAX, щоб упевнитися, що вихідні дані знаходяться у межах від 0 до 255, неважливо, яким є вхідний діапазон. Створимо функцію для виконання перетворення у прямокутну функцію та сконвертуємо у 8-бітні пікселі таким чином:

// Конвертуємо ряд чи колонку матриці плаваючих чисел у
// прямокутне 8-бітне зображення, яке можна показати чи зберегти.
// Масштабуємо значення в діапазон від 0 до 255.
Mat getImageFrom1DFloatMat(const Mat matrixRow, int height)
{
// Робимо прямокутну форму замість одного ряду.
Mat rectangularMat = matrixRow.reshape(1, height);
// Масштабуємо значення між 0 та 255 та зберігаємо їх
// у вигляді звичайного зображення з 8-бітними пікселями
Mat dst;
normalize(rectangularMat, dst, 0, 255, NORM_MINMAX,
CV_8UC1);
return dst;
}

Щоб легше налагоджувати код OpenCV та перевіряти структуру даних cv::Algorithm, можна скористатися файлами ImageUtils.cpp та ImageUtils.h для виводу інформації про сv::Mat:
Mat img = ...;
printMatInfo(img, "Моє зображення");

Ви побачите дещо подібне у консолі:
Моє зображення: 640w480h 3ch 8bpp, range[79,253][20,58][18,87]
Ця стрічка вказує на те, що зображення має ширину 640 елементів та висоту 480 (тобто кадр з розмірами 640х480 чи матриця розмірності 480х640, залежно від погляду) з трьома 8-бітними каналами на піксель (тобто, звичайне BGR зображення), і для кожного каналу зазначені мінімальне та максимальне значення.

Примітка
Можна також надрукувати власне зміст зображення чи матриці за допомогою функції printMat() замість printMatInfo(). Вона досить корисна для огляду звичайних та багатоканальних матриць плаваючих чисел, оскільки їх доволі важко відслідковувати початківцям.

Код класу ImageUtils практично повністю заснований на С інтерфейсі бібліотеки OpenCV, але з часом включатиме все більше методів С++. Остання верcія доступна за адресою http://shervinemami.info/openCV.html.

Середнє обличчя

Алгоритми Eigenfaces та Fisherfaces спочатку обраховують середнє обличчя математично для усіх тренувальних зображень, тому від кожного можна відняти його та отримати кращі результати розпізнавання. Розглянемо, яким саме воно є. Іншою назвою цієї характеристики в реалізаціях Eigenfaces та Fisherfaces є "mean", описаний таким чином:

Mat averageFace = model->get<Mat>("mean");
printMatInfo(averageFace, "averageFace (ряд)");
// конвертуємо ряд матриці плаваючих чисел у звичайне 8-бітне зображення
averageFace = getImageFrom1DFloatMat(averageFace, faceHeight);
printMatInfo(averageFace, "averageFace");
imshow("середнє обличчя", averageFace);

Ви маєте побачити на екрані зображення середнього обличчя, подібного до такого збільшеного фрагмента, що є комбінацією чоловіка, жінки та дитини. Також у консолі надруковано схожий текст:

averageFace (ряд): 4900w1h 1ch 64bpp, range[5.21,251.47]
averageFace: 70w70h 1ch 8bpp, range[0,255]
Малюнок матиме такий вигляд:
averageFace (ряд) - це проста матриця 64-бітних плаваючих значень, а просто averageFace - це прямокутне зображення з 8-бітних пікселів, які покривають межі від 0 до 255.
Власні значення, власні обличчя та обличчя Фішера
Виведемо на екран власні значення у вигляді тексту:
Mat eigenvalues = model->get<Mat>("eigenvalues");
printMat(eigenvalues, "eigenvalues");

Для кожного власного обличчя існує єдине власне значення, тому якщо наявні чотири знімки обличчя для трьох осіб, отримаємо вектор колонки з 12 власними значеннями, відсортованими від найкращих до найгірших:
власні значення: 1w18h 1ch 64bpp, range[4.52e+04,2.02836e+06]
2.03e+06
1.09e+06
5.23e+05
4.04e+05
2.66e+05
2.31e+05
1.85e+05
1.23e+05
9.18e+04
7.61e+04
6.91e+04
4.52e+04

Для методу Fisherfaces існує єдине власне значення для кожної особи, тому, якщо матимемо по чотири зображення облич для трьох людей, отримаємо верктор ряду з двома власними значеннями:
власні значення: 2w1h 1ch 64bpp, range[152.4,316.6]
317, 152

Щоб розглянути власні вектори у вигляді зображень Eigenfaces чи Fisherfaces, ми повинні виокремити їх у вигляді колонок з великої матриці власних векторів. Оскільки дані OpenCV та С/C++ звичайно зберігаються у матрицях, відсортованих відносно рядів, ми повинні скористатися функцією Mat::clone() для того, щоб мати можливість розширити дані, інакше ми не зможемо зробити прямокутну форму. Маючи матрицю з колонок, можемо зобразити власні вектори за допомогою функції getImageFrom1DFloatMat(), як це було зроблено раніше для середнього обличчя:

// Отримаємо власні вектори
Mat eigenvectors = model->get<Mat>("eigenvectors");
printMatInfo(eigenvectors, "eigenvectors");
// Покажемо накращі 20 власних облич
for (int i = 0; i < min(20, eigenvectors.cols); i++) {
// Створимо продовжений вектор із власного вектора #i
Mat eigenvector = eigenvectors.col(i).clone();
Mat eigenface = getImageFrom1DFloatMat(eigenvector,
faceHeight);
imshow(format("Eigenface%d", i), eigenface);
}

Наступний малюнок ілюструє власні вектори у вигляді зображень. Можна бачити, що для трьох людей з чотирма зображеннями облич кожного існують 12 власних облич (ліворуч) та 2 обличчя Фішера (праворуч).

Обличчя власні та Фішера мають певні подібності з характеристиками обличчя, але насправді не схожі на них. Так відбувається тому, що вони отримані відніманням від обличчя середнього зображення. Число вказує на те, яке саме це власне обличчя, тому що вони завжди впорядковані від найважливішого до найменш важливого, і якщо ви маєте 50 чи більше власних облич, решту можна відкинути, вона вказуватимуть лише на випадкові шуми.

Етап 4. Розпізнавання облич

Маючи алгоритм машинного навчання Eigenfaces чи Fisherfaces, натренований на основі зображень та відміток, ми готові визначити особу лише по зображенню обличчя! Останній крок називають розпізнаванням чи ідентифікацією.

Ідентифікація обличчя: розпізнаємо людей

Завдяки класу FaceRecognizer бібліотеки OpenCV можемо визначити людину на фотографії за допомогою методу FaceRecognizer::predict() так:
int identity = model->predict(preprocessedFace);

Значенням ідентичності є номер відмітки, використаний для збору облич для тренування.
Наприклад, 0 позначає першу особу, 1 - другу і так далі. Проблема з цією ідентифікацією в тому, що вона завжди визначить одну із заданих осіб, навіть якщо вхідні дані зображають невідому особу чи автомобіль. Програма все ще повідомить, хто саме зображений на фото з вірогідною людиною, тому результату навряд чи можна довіряти! Одним з рішень є пошук метрики впевненості, щоб ми мали можливість перевірити надійність результату, а якщо виявиться, що впевненість надто низька, припускаємо, що це невідома особа.

Верифікація обличчя: підтвердження його приналежності певній особі

Щоб перевірити, чи результат вказує на потрібну особу або невідому, здійснюємо верифікацію (яку називають ще аутентифікацією), щоб обчислити метрику впевненості, яка вкаже на рівень схожості обличчя з певною особою (на відміну від ідентифікації, що порівнює один фрагмент з багатьма обличчями людей.

Клас FaceRecognizer бібліотеки OpenCV здатний повертати метрику впевненості за допомогою виклику функції predict(), але, на жаль, вона заснована на відстані у підпросторі власних векторів, тому не дуже надійна. Скористаємось методом, який відновить зображення обличчя за допомогою власних векторів та власних значень, а також порівняє отриманий результат з вхідними даними. Якщо в тренувальному наборі збережено багато облич однієї особи, етап відновлення має відбутися без жодних проблем, на основі вивчених власних векторів та власних значень, якщо ж облич для певної особи немає (чи немає жодного знімка за однакових умов освітлення та з тим же виразом, як у тестових примірниках), отримане зображення суттєво відрізнятиметься від вхідного, тобто воно, скоріше за все, належить невідомій особі.

Як сказано раніше, алгоритми Eigenfaces та Fisherfaces базуються на припущенні, що зображення грубо представлене у вигляді набору власних векторів (особливі кадри обличчя) та власних значень (відношення змішування). Якщо ми скомбінуємо ці характеристики для одного обличчя тренувального набору, то зможемо здобути досить схожу на оригінал копію. Те ж стосується інших кадрів, подібних до елементів тренувального набору - скомбінувавши натреновані власні вектори із власними значеннями зі схожого тестового зображення, зможемо відновити фрагмент, який виглядатиме його копією.

Знову ж таки, клас FaceRecognizer бібліотеки OpenCV дозволяє легко відновлювати обличчя із вхідного зображення за допомогою функції subspaceProject() для проєктування його у власний простір та subspaceReconstruct(), щоб повернутися з власного простого у простір зображення. Нюанс полягає в тому, що нам треба сконвертувати його з матриці рядів плаваючих значень у прямокутне 8-бітне зображення (як ми робили, коли зображали середнє обличчя та власні обличчя), але ми не бажаємо нормувати дані, адже вже маємо ідеальний масштаб для порівняння з оригінальним зображенням. Якщо нормуємо дані, вони матимуть іншу яскравість та контраст, ніж вхідний кадр, у такому разі порівнювати схожість кадрів лише на основі відносної похибки L2 складно. Відповідний фрагмент коду матиме такий вигляд:

// отримуємо потрібні дані з моделі FaceRecognizer
Mat eigenvectors = model->get<Mat>("eigenvectors");
Mat averageFaceRow = model->get<Mat>("mean");
// проєктуємо вхідне зображення на власний простір
Mat projection = subspaceProject(eigenvectors, averageFaceRow,
preprocessedFace.reshape(1,1));
// генеруємо відновлене обличчя з власного простору
Mat reconstructionRow = subspaceReconstruct(eigenvectors,
averageFaceRow, projection);
// перетворюємо ряд у прямокутну форму зображення.
Mat reconstructionMat = reconstructionRow.reshape(1,
faceHeight);
// конвертуємо плаваючі значення пікселів у звичайні 8-бітні uchar
Mat reconstructedFace = Mat(reconstructionMat.size(), CV_8U);
reconstructionMat.convertTo(reconstructedFace, CV_8U, 1, 0);

Наступний малюнок зображає два типових відновлених обличчя. З лівого боку маємо чітке зображення відомої людини, а з правого - неточне, бо це була невідома особа чи відома, але за інших умов освітлення, з інакшим виразом обличчя чи напрямом погляду.

Тепер можемо обрахувати, наскільки відновлені обличчя схожі на вхідні кадри за допомогою функції getSimilarity(), яку ми створили для порівняння фрагментів. Нагадаємо, значення, менше за 0.3, вказує на високий рівень подібності. Що стосується власних облич, для кожного з них існує єдиний власний вектор, відновлення ефективне, тому значення порогу звичайно складатиме 0.5. Обличчя Фішера включають один власний вектор для кожної особи, тому відновлення на надає якісні результати. У такому разі потрібен високий рівень порогу, наприклад 0.7. Це робиться таким чином:

similarity = getSimilarity(preprocessedFace,
reconstructedFace);
if (similarity > UNKNOWN_PERSON_THRESHOLD) {
identity = -1; // Невідома особа.
}

Тепер можна просто вивести значення ідентичності в консолі або використати його як завгодно!
Пам'ятайте, що методи розпізнавання та верифікації обличчя надійні лише в умовах, у яких здійснено тренування. Щоб отримати прийнятну точність розпізнавання, потрібно впевнитися в тому, що тренувальний набір покриє величезнку кількість різних умов освітлення, виразів обличчя та кути для перевірок. Етап попередньої обробки обличчя допоміг дещо знизити відмінності світлових умов та повороту на площині (якщо особа нахиляє голову у напрямку лівого чи правого плеча), але для інших відмінностей, наприклад повороту за площиною (якщо людина повертає голову вліво чи вправо), він працюватиме лише за достатнього покриття тренувального набору.

Фінальні правки: збереження та завантаження файлів

Звичайно, можна створити метод командної стрічки для оброблення вхідних файлів та збереження їх на диску або навіть здійснити пошук обличчя, попередню обробку чи розпізнавання у вигляді веб-сервісу і так далі. У подібних проєктах досить легко додавати бажану функціональність за допомогою функцій збереження та завантаження класу FaceRecognizer. Це дає також змогу користуватися заздалегідь вивченими даними.

Зберегти натреновану модель у вигляді файла XML чи YML дуже просто:
model->save("trainedModel.yml");
Якщо потрібно додати більше даних у тренувальний набір, можна зберегти масив попередньо оброблених облич та відміток. Наприклад, це приблизний код для завантаження тренувальної моделі з файлу. Врахуйте, ви маєте визначити алгоритм розпізнавання обличчя (наприклад FaceRecognizer.Eigenfaces чи FaceRecognizer.Fisherfaces), використаний для її створення:

string facerecAlgorithm = "FaceRecognizer.Fisherfaces";
model = Algorithm::create<FaceRecognizer>(facerecAlgorithm);
Mat labels;
try {
model->load("trainedModel.yml");
labels = model->get<Mat>("labels");
} catch (cv::Exception &e) {}
if (labels.rows <= 0) {
cerr << "ПОМИЛКА: Не вдалося завантажити тренувальні дані з файла "
"[trainedModel.yml]!" << endl;
exit(1);
}

Фінальні правки: створюємо гарний інтерактивний інтерфейс

Хоча вказаного коду й достатньо для системи розпізнавання цілого обличчя, все ще потрібен спосіб розміщення та використання даних. Для дослідження більшість систем розпізнавання обличчя в якості вхідного параметра застосовує текстовий файл зі списком зображень, а також інші важливі дані, наприклад дійсне ім'я чи ідентичність особи і координати дійсних пікселів фрагментів (еталонні положення центра обличчя чи очей). Скоріше за все, ці характеристики будуть зібрані іншою системою розпізнавання.

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

Але система розпізнавання обличчя у цьому розділі розроблена для навчання та практичних цілей, а не конкурування з останніми дослідженими методами, корисно мати зручний інтерфейс користувача, що дозволяє збирати колекцію облич, тренувати та тестувати в інтерактивному режимі. Тому в цій частині опишемо графічний інтерфейс, що надасть подібні можливості. Читач може скористатися кодом, що постачається разом із книгою, або модифікувати під свої вимоги, або проігнорувати та розрозбити власний інтерфейс користувача для здійснення описаних технік розпізнавання.

Оскільки графічний інтерфейс має виконувати декілька задач, створимо набір режимів чи станів та можливість перемикатися між ними:
Налаштування. Завантажує та встановлює дані й камеру.
Пошук. Знаходить обличчя та виводить їх з попередньою обробкою на екран до тих пір, поки користувач не натисне кнопку "Додати особу".
Збір даних. Акумулює зображення обличчя певної людини, поки користувач не здійснить натисне кнопку миші десь у вікні. Також виводить останній кадр обличчя для кожної особи. Користувач обирає або одне з наявних облич, або кнопку "Додати особу", щоб зібрати відповідні фрагменти для різних людей.
Тренування. У цьому стані система тренується на основі всіх зібраних облич для всіх наявних осіб.
Розпізнавання. Включає виділення розпізнаної особи та відображення метрики впевненості. Користувач або обирає одну з осіб, або натискає кнопку "Додати особу", щоб повернутися на етап збору.

Щоб завершити роботу, користувач має можливість натиснути Escape коли завгодно. Також додамо режим "Видалити все", який перезавантажує роботу системи розпізнавання обличчя та кнопку "Налагодження", яка виводить на екран додаткову інформацію. Можна також створити змінну переліку режимів та виводити поточний.

Малюємо елементи графічного інтерфейсу

Щоб вказати на екрані поточний режим, створимо функцію для полегшення виводу тексту. Бібліотека OpenCV постачається із функцію cv::putText(), що має можливості обирати один з декількох шрифтів та згладжування, але розмістити текст у бажаній позиції нелегко. На щастя, існує також функція cv::getTextSize(), яка обчислює обмежувальний прямокутник навколо тексту, тому можемо запрограмувати функцію-обгортку для полегшення цієї процедури. Ми бажаємо отримати можливість розміщувати текст вздовж будь-якої границі вікна та впевнитися, що він видимий повністю, і дозволити вивід декількох стрічок чи слів поряд без накладання. Запропонуємо таку функцію, яка дозволить вказувати положення зліва чи справа, зверху чи знизу, а також повертати обмежувальний прямокутник, аби мати можливість легко зображати декілька стрічок тексту в позиції будь-якого кута чи границі вікна:

// Малюємо текст на зображенні. За замовчуванням встановлена
// позиція зверху зліва, тому вкажіть від'ємне х для правої
// сторони, і/або від'ємне y для позиції знизу. Повертає
// обмежувальний прямокутник навколо намальованого тексту.
Rect drawString(Mat img, string text, Point coord, Scalar
color, float fontScale = 0.6f, int thickness = 1,
int fontFace = FONT_HERSHEY_COMPLEX);

Зупинимось на питанні виведення поточного режиму на графічному інтерфейсі. Оскільки фонове зображення отримане з камери, якщо ми просто намалюємо текст на його поверхні, він матиме той же колір! Тому додамо чорну тінь на відстані 1 піксель завширшки від тексту. Також намалюємо стрічку допоміжного тексту, щоб користувач міг здійснити певні кроки. Ось приклад виводу за допомогою функції drawString():

string msg = "Натисніть "Додати особу", якщо готові збирати обличчя..";
// Намалювати чорну тінь та білий текст.
float txtSize = 0.4;
int BORDER = 10;
drawString(displayedFrame, msg, Point(BORDER, -BORDER-2),
CV_RGB(0,0,0), txtSize);
Rect rcHelp = drawString(displayedFrame, msg, Point(BORDER+1,
-BORDER-1), CV_RGB(255,255,255), txtSize);

Частковий скріншот показує режим та дані в нижній частині графічного інтерфейсу, розміщені на поверхні кадру камери:
Як вже зазначено, потрібно декілька кнопок, тому створимо функцію для їх малювання:

// Малюємо кнопку графічного інтерфейсу на зображенні за допомогою drawString().
// Можемо задати значення minWidth, щоб декілька кнопок мали однакову ширину.
// Повертає обмежувальний прямокутник навколо кнопки.
Rect drawButton(Mat img, string text, Point coord,
int minWidth = 0)
{
const int B = 10;
Point textCoord = Point(coord.x + B, coord.y + B);
// Отримуємо обмежувальний прямокутник навколо тексту.
Rect rcText = drawString(img, text, textCoord,
CV_RGB(0,0,0));
// Малюємо заповнений прямокутник навколо тексту.
Rect rcButton = Rect(rcText.x - B, rcText.y – B,
rcText.width + 2*B, rcText.height + 2*B);
// Встановлюємо мінімальну ширину кнопки.
if (rcButton.width < minWidth)
rcButton.width = minWidth;
// Створюємо напівпрозорий білий прямокутник.
Mat matButton = img(rcButton);
matButton += CV_RGB(90, 90, 90);
// Малюємо непрозору білу границю.
rectangle(img, rcButton, CV_RGB(200,200,200), 1, CV_AA);
// Виводимо на екран текст.
drawString(img, text, textCoord, CV_RGB(10,55,20));
return rcButton;
}

Тепер за допомогою функції drawButton() створимо декілька кнопок, які завжди зображатимуться у верхній лівій частині графічного інтерфейсу, як показано далі:

Як вже зазначалося, програма з графічним інтерфейсом має декілька режимів, між якими можна перемикатися (як у паттерні FSM), починаючи з режиму Startup. Поточний режим зберігатимемо у змінній m_mode.

Режим налаштування

У цьому режимі потрібно завантажити XML-файли детектора для розпізнавання обличчя та очей, а також налаштувати вебкамеру. Ці процеси ми вже пояснили. Також створимо головне вікно графічного інтерфейсу із функцією зворотнього виклику, яку бібліотека OpenCV активуватиме кожен раз, коли користувач переміщує чи натискає кнопку миші у вікні. Крім цього, бажано встановити роздільну здатність камери у прийнятні межі, наприклад 640х480, якщо це значення підтримується. Це робиться таким чином:

// Створюємо вікно графічного інтерфейсу для виводу на екран.
namedWindow(windowName);
// Викликаємо функцію onMouse(), коли користувач натискає кнопку миші.
setMouseCallback(windowName, onMouse, 0);
// Встановлюємо розподільну здатність камери. Працює лише на деяких системах.
videoCapture.set(CV_CAP_PROP_FRAME_WIDTH, 640);
videoCapture.set(CV_CAP_PROP_FRAME_HEIGHT, 480);
// Налаштування вже відбулося, тому продовжимо з режиму пошуку.
m_mode = MODE_DETECTION;

Режим пошуку

У цьому режимі ми шукаємо обличчя та очі, малюємо прямокутники або кола навколо них для вказівки на результат та показуємо попередньо оброблені обличчя. Фактично, ми бажаємо зобразити їх не залежно від режиму. Єдиною особливістю пошуку є перехід до наступного етапу (збір даних), коли користувач натискає на кнопку "Додати особу".

Якщо ви пам'ятаєте з етапу пошуку, описаного раніше в цьому розділі, вихідні дані такі:
Mat preprocessedFace: попередньо оброблене обличчя (якщо обличчя та очі розпізнані)
Rect faceRect: координати знайденого фрагмента обличчя
Point leftEye, rightEye: координати знайдених центрів лівого та правого ока.
Тому ми маємо перевірити, чи повернене попередньо оброблене обличчя, та намалювати прямокутник і кола, описані навколо знайдених обличчя та очей:

bool gotFaceAndEyes = false;
if (preprocessedFace.data)
gotFaceAndEyes = true;
if (faceRect.width > 0) {
// Малюємо згладжений прямокутник навколо знайденого обличчя.
rectangle(displayedFrame, faceRect, CV_RGB(255, 255, 0), 2,
CV_AA);
// Малюємо яскраво-блакитні згладжені кола для двох очей.
Scalar eyeColor = CV_RGB(0,255,255);
if (leftEye.x >= 0) { // Перевіряємо, чи знайдене ліве око
circle(displayedFrame, Point(faceRect.x + leftEye.x,
faceRect.y + leftEye.y), 6, eyeColor, 1,
CV_AA);
}
if (rightEye.x >= 0) { // Перевіряємо, чи знайдене праве око
circle(displayedFrame, Point(faceRect.x + rightEye.x,
faceRect.y + rightEye.y), 6, eyeColor, 1,
CV_AA);
}
}

Накладемо поточне попередньо оброблене обличчя у верхній центральній частині вікна таким чином:
int cx = (displayedFrame.cols - faceWidth) / 2;
if (preprocessedFace.data) {
// Обираємо BGR версію обличчя, оскільки результати маємо у такому форматі.
Mat srcBGR = Mat(preprocessedFace.size(), CV_8UC3);
cvtColor(preprocessedFace, srcBGR, CV_GRAY2BGR);
// Обираємо регіон інтересу результату.
Rect dstRC = Rect(cx, BORDER, faceWidth, faceHeight);
Mat dstROI = displayedFrame(dstRC);
// Копіюємо пікселі з вхідної матриці у вихідну.
srcBGR.copyTo(dstROI);
}
// Малюємо згладжені границі навколо обличчя.
rectangle(displayedFrame, Rect(cx-1, BORDER-1, faceWidth+2,
faceHeight+2), CV_RGB(200,200,200), 1, CV_AA);

Наступний скріншот зображає графічний інтерфейс у режимі пошуку. Попередньо оброблене обличчя показане у верхній центральній частині вікна, а знайдені обличчя та очі промарковані:

Режим збору даних

Ми входимо в режим збирання даних, коли користувач натискає кнопку "Додати особу", щоб просигналізувати бажання почати збирання облич для нової людини. Як зазначено раніше, ми обмежили кількість знімків до одного в секунду, причому їхній вигляд має суттєво відрізнятися від попереднього. І пам'ятайте, ми бажаємо збирати не лише попередньо оброблені обличчя, а також їх віддзеркалення.

У режимі збирання даних ми також бажаємо показати останнє обличчя кожної відомої особи та дозволити користувачу натискати на одну з наявних осіб чи доєднати більше примірників облич або взагалі натиснути кнопку "Додати людину" і розширити колекцію зображенням нової особи. Користувач має здійснити клік в межах вікна, щоб перейти в наступний режим, тренування.

Спершу маємо зберегти посилання на останнє зібране обличчя для кожної особи. Це здійснюємо шляхом оновлення масиву цілих чисел m_latestFaces, який зберігає лише індекси для людини, з великого масиву preprocessedFaces (тобто колекції всіх облич всіх людей). Оскільки ми також зберігаємо віддзеркалене зображення, маємо посилатися на другий з кінця елемент, а не на останній. Тому потрібно додати відповідний код до фрагменту, що додає нове обличчя (та його віддзеркалений варіант) до масиву preprocessedFaces таким чином:

Збережемо посилання на останнє обличчя кожної людини.
m_latestFaces[m_selectedPerson] = preprocessedFaces.size() - 2;
Варто лише пам'ятати про необхідність завжди збільшувати чи зменшувати масив m_latestFaces, коли додаються чи видаляються нові зображення облич (наприклад, у результаті натискання кнопки "Додати особу"). Виведемо останні зображення для кожної наявної людини у правому боці вікна (у режимах збирання даних та розпізнавання) так:

m_gui_faces_left = displayedFrame.cols - BORDER - faceWidth;
m_gui_faces_top = BORDER;
for (int i=0; i<m_numPersons; i++) {
int index = m_latestFaces[i];
if (index >= 0 && index < (int)preprocessedFaces.size()) {
Mat srcGray = preprocessedFaces[index];
if (srcGray.data) {
// Збираємо обличчя BGR, оскільки результат також має цей формат.
Mat srcBGR = Mat(srcGray.size(), CV_8UC3);
cvtColor(srcGray, srcBGR, CV_GRAY2BGR);
// Отримуємо вихідний регіон інтересу.
int y = min(m_gui_faces_top + i * faceHeight,
displayedFrame.rows - faceHeight);
Rect dstRC = Rect(m_gui_faces_left, y, faceWidth,
faceHeight);
Mat dstROI = displayedFrame(dstRC);
// Копіюємо пікселі з вихідної матриці в цільову.
srcBGR.copyTo(dstROI);
}
}
}

Ми також бажаємо виділити поточну особу, для якої збираємо фотографії, за допомгою товстої червоної границі навколо обличчя.
Це робиться таким чином:
if (m_mode == MODE_COLLECT_FACES) {
if (m_selectedPerson >= 0 &&
m_selectedPerson < m_numPersons) {
int y = min(m_gui_faces_top + m_selectedPerson *
faceHeight, displayedFrame.rows –
faceHeight);
Rect rc = Rect(m_gui_faces_left, y, faceWidth,
faceHeight);
rectangle(displayedFrame, rc, CV_RGB(255,0,0), 3,
CV_AA);
}
}

Наступний частковий скріншот показує типовий результат виводу, коли зібрані обличчя для декількох людей. Користувач може натиснути на будь-яку з осіб у верхній правій частині, щоб зібрати більше даних для неї.

Режим тренування

Коли користувач нарешті натискає кнопку миші у вікні, алгоритм розпізнавання здійснить процедуру тренування на основі зібраних облич. При цьому варто впевнитися, що зібрано достатню кількість даних, інакше програма матиме збій у роботі. В загальному випадку, потрібне як мінімум одне обличчя в тренувальному наборі (наяна принаймні одна особа). Але алгоритм Fisherfaces, наприклад, порівнює між собою декількох людей, без відповідних даних збоєм завершиться й він. Тому ми мусимо перевірити, чи обрано цей метод для розпізнавання облич. Якщо це так, потрібні зображення як мінімум двох людей з обличчями, інакше лише однієї. За недостатності даних програма повернеться у режим збирання, щоб користувач міг додати більше облич для тренування.

Щоб перевірити наявність як мінімум двох людей із зібраними обличчями, коли користувач натискає на кнопку "Додати особу", новий об'єкт додається лише тоді, коли немає іншого незаповненого (тобто особу додано, але зібраних облич для неї немає). Ми також можемо впевнитися, що якщо наявні лише дві людини і ми використовуємо алгоритм Fisherfaces, потрібно встановити посилання m_latestFaces на останню людину в режимі збирання даних. m_latestFaces[i] отримує значення -1, якщо зображень облич для неї ще не маємо, та 0 чи вище, якщо обличчя для цієї людини додано. Це робиться таким чином:

// Перевіряємо, чи достатньо даних для тренування.
bool haveEnoughData = true;
if (!strcmp(facerecAlgorithm, "FaceRecognizer.Fisherfaces")) {
if ((m_numPersons < 2) ||
(m_numPersons == 2 && m_latestFaces[1] < 0) ) {
cout << "Для алгоритму Fisherfaces потрібно не менше двох осіб!" << endl;
haveEnoughData = false;
}
}
if (m_numPersons < 1 || preprocessedFaces.size() <= 0 ||
preprocessedFaces.size() != faceLabels.size()) {
cout << "Перед початком тренування треба зібрати дані!" << endl;
haveEnoughData = false;
}
if (haveEnoughData) {
// Вивчаємо зібрані обличчя за допомогою методів Eigenfaces та Fisherfaces.
model = learnCollectedFaces(preprocessedFaces, faceLabels,
facerecAlgorithm);
// Завершивши процес тренування, можемо почати розпізнавання!
m_mode = MODE_RECOGNITION;
}
else {
// Недостатньо тренувальних даних, повернемося до режиму збирання!
m_mode = MODE_COLLECT_FACES;
}

Тренування може зайняти долі секунди, декілька секунд чи навіть хвилин, залежно від кількості зібраних даних. Коли процедура завершиться, система автоматично увійде в режим розпізнавання.

Режим розпізнавання

У цьому режимі поряд з попередьно обробленим обличчя показана метрика впевненості, аби користувач міг упевнитися, наскільки надійним є розпізнавання. Якщо значення більше за певний поріг, навколо людини намалюється зелений прямокутник. Користувач може доєднати нові обличчя для подальшого тренування, натиснувши кнопку "Додати особу" або обравши одну з наявних, що змусить програму повернутися на етап збирання даних.

Ми вже здобули розпізнані показники ідентичності та схожості з відновленим обличчям як зазначено раніше. Щоб вивести метрику впевненості на екрані, значення схожості вкажемо в межах від 0 до 0.5 для високого рівня та від 0.5 до 1.0 - для низького, у такому разі зможемо відняти її від одиниці, щоб отримати відповідне значення впевненості від 0.0 до 1.0. Потім просто намалюємо заповнений прямокутник на основі отриманого відношення:

int cx = (displayedFrame.cols - faceWidth) / 2;
Point ptBottomRight = Point(cx - 5, BORDER + faceHeight);
Point ptTopLeft = Point(cx - 15, BORDER);
// Намалюємо сіру лінію, що показує поріг для "невідомої" людини.
Point ptThreshold = Point(ptTopLeft.x, ptBottomRight.y -
(1.0 - UNKNOWN_PERSON_THRESHOLD) * faceHeight);
rectangle(displayedFrame, ptThreshold, Point(ptBottomRight.x,
ptThreshold.y), CV_RGB(200,200,200), 1, CV_AA);
// Обмежимо рівень впевненості у проміжок від 0 до 1, щоб вмістити результат.
double confidenceRatio = 1.0 - min(max(similarity, 0.0), 1.0);
Point ptConfidence = Point(ptTopLeft.x, ptBottomRight.y -
confidenceRatio * faceHeight);

// Зобразимо яскраво-блакитний прямокутник впевненості.
rectangle(displayedFrame, ptConfidence, ptBottomRight,
CV_RGB(0,255,255), CV_FILLED, CV_AA);
// Виведемо сірі границі прямокутника.
rectangle(displayedFrame, ptTopLeft, ptBottomRight,
CV_RGB(200,200,200), 1, CV_AA);
Щоб виділити розпізнану особу, намалюємо зелений прямокутник навколо обличчя:
if (identity >= 0 && identity < 1000) {
int y = min(m_gui_faces_top + identity * faceHeight,
displayedFrame.rows - faceHeight);
Rect rc = Rect(m_gui_faces_left, y, faceWidth, faceHeight);
rectangle(displayedFrame, rc, CV_RGB(0,255,0), 3, CV_AA);
}

Далі проілюстровано типові результати виводу у режимі розпізнавання, що показують значення впевненості поряд з попередньо обробленим обличчям у верхній центральній частині та виділяє розпізнану людину у верхньому правому куті.

Перевірка та обробка натискання кнопок миші

Маючи всі потрібні елементи графічного інтерфейсу, перейдемо до обробки подій миші. Налаштувавши вікно, ми повідомили бібліотеці OpenCV про функцію зворотнього виклику onMouse(). Нас не хвилює рух миші, лише кліки, тому пропустимо події, які не стосуються лівої кнопки миші:

void onMouse(int event, int x, int y, int, void*)
{
if (event != CV_EVENT_LBUTTONDOWN)
return;
Point pt = Point(x,y);
... (handle mouse clicks) ...
}

Оскільки ми вже маємо прямокутні межі кнопок, потрібно лише перевірити, чи знаходиться позиція курсора у будь-якому відповідному регіоні за допомогою функції inside() бібліотеки OpenCV. Процедуру можна здійснити для усіх створених кнопок.

Коли користувач натискає кнопку "Додати особу", ми додаємо одиницю до змінної m_numPersons, виділяємо простір у змінній m_latestFaces, обираємо нову людину для процедури та переходимо в режим збирання даних (не важливо, у якому були раніше).

Але існує одна складність; щоб впевнитися, що ми маємо як мінімум одне обличчя для кожної особи на етапі тренування, виділимо простір для нової лише тоді, коли немає інших осіб без зображень. У такому разі ми матимемо можливість перевірити значення змінної m_latestFaces [m_numPersons-1] та побачити, чи додані обличчя для кожної особи. Здійснимо це так:

if (pt.inside(m_btnAddPerson)) {
// Впевнимося, що не існує інших осіб без зібраних даних.
if ((m_numPersons==0) ||
(m_latestFaces[m_numPersons-1] >= 0)) {
// Додаємо нову особу.
m_numPersons++;
m_latestFaces.push_back(-1);
}
m_selectedPerson = m_numPersons - 1;
m_mode = MODE_COLLECT_FACES;
}

Цей метод можна використати для тестування інших кнопок, наприклад за допомогою флагу налаштування:
else if (pt.inside(m_btnDebug)) {
m_debug = !m_debug;
}

Щоб виконати обробку кнопки "Видалити все", потрібно звільнити різні структури даних, розміщені локально у головному циклі (недоступному функції зворотнього виклику для подій миші), тому перейдемо в режим видалення, щоб мати таку можливість. Ми також маємо обробити події натискання мишею в межах вікна (не кнопок). Якщо користувач вибере одне з облич праворуч, маємо обрати особу та перейти в режим збирання даних. А якщо він натисне у вікні у цьому режимі, потрібно перейти у стан тренування. Це можна зробити так:

else {
// Перевіряємо, чи користувач натиснув на обличчя списку.
int clickedPerson = -1;
for (int i=0; i<m_numPersons; i++) {
if (m_gui_faces_top >= 0) {
Rect rcFace = Rect(m_gui_faces_left,
m_gui_faces_top + i * faceHeight,
faceWidth, faceHeight);
if (pt.inside(rcFace)) {
clickedPerson = i;
break;
}
}
}
// Виділяємо іншу людину, якщо користувач натиснув на її обличчя.
if (clickedPerson >= 0) {
// Змінюємо поточну особу та збираємо фотографії.
m_selectedPerson = clickedPerson;
m_mode = MODE_COLLECT_FACES;
}
// Інакше користувач здійснив клік в центрі вікна.
else {
// Переходимо у стан тренування з режиму збирання даних.
if (m_mode == MODE_COLLECT_FACES) {
m_mode = MODE_TRAINING;
}
}
}

Висновки

У цьому розділі показано всі кроки, потрібні для створення застосунків розпізнавання облич, що діють у режимі реального часу, з достатньою попередньою обробкою, що дозволяє певні відмінності між умовами тренувального та тестувального наборів, за допомогою простих алгоритмів. Для знаходження позиції обличчя на кадрі камери ми скористалися процедурою пошуку, декількома видами попередньої обробки для зменшення ефектів різних умов освітлення, орієнтації камери та обличчя, виразів. Потім натренували системи машинного навчання алгоритмів Eigenfaces та Fisherfaces на основі зібраних даних та здійснили розпізнавання обличчя, аби вказати, ким є особа, на основі процедури верифікації, яка надає метрику впевненості у випадках, якщо та невідома.

Замість створення інструменту командної стрічки, який обробляє файли зображень в режимі офлайн, ми об'єднали попередні кроки у цілісний графічний інтерфейс, який дозволяє негайно використовувати систему розпізнавання обличчя. Ви повинні мати можливість модифікувати поведінку системи заради власних цілей, наприклад дозволити автоматичний вхід до комп'ютера, або підвищити надійність розпізнавання на основі нещодавніх досліджень, щоб потенційно покращити кожен крок програми, поки вона не підходитиме специфічним потребам. Наприклад, можна вдосконалити попередню обробку облич, або застосувати кращий алгоритм машинного навчання чи верифікації обличчя за методами, що описані на ресурсах http://www.face-rec.org/algorithms/ та http://www.cvpapers.com

Посилання

Rapid Object Detection using a Boosted Cascade of Simple Features, P. Viola and M.J. Jones,
Proceedings of the IEEE Transactions on CVPR 2001, Vol. 1, pp. 511-518
An Extended Set of Haar-like Features for Rapid Object Detection, R. Lienhart and J. Maydt,
Proceedings of the IEEE Transactions on ICIP 2002, Vol. 1, pp. 900-903
Face Description with Local Binary Patterns: Application to Face Recognition, T. Ahonen, A.
Hadid and M. Pietikainen, Proceedings of the IEEE Transactions on PAMI 2006, Vol. 28,
Issue 12, pp. 2037-2041
Learning OpenCV: Computer Vision with the OpenCV Library, G. Bradski and A. Kaehler,
pp. 186-190, O'Reilly Media.

Eigenfaces for recognition, M. Turk and A. Pentland, Journal of Cognitive Neuroscience 3, pp.
71-86
Eigenfaces vs. Fisherfaces: Recognition using class specific linear projection, P.N. Belhumeur,
J. Hespanha and D. Kriegman, Proceedings of the IEEE Transactions on PAMI 1997, Vol. 19,
Issue 7, pp. 711–720
Face Recognition with Local Binary Patterns, T. Ahonen, A. Hadid and M. Pietikainen,
Computer Vision - ECCV 2004, pp. 469–48

Внимание! Этот перевод, возможно, ещё не готов.
Его статус: перевод редактируется

Переведено на Нотабеноиде
http://notabenoid.org/book/81521/501840

Переводчики: Atronax

Настройки

Готово:

100.00% КП = 1.0