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

← →
Английский оригинал Перевод на украинский

Сожалеем, но текст оригинала доступен только зарегистрированным пользователям.

Для кожного власного обличчя існує єдине власне значення, тому якщо наявні чотири знімки обличчя для трьох осіб, отримаємо вектор колонки з 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

Atronax 22.03.21 в 15:21

0

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

Atronax 22.03.21 в 15:21

0

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

Atronax 22.03.21 в 15:22

0

// Отримаємо власні вектори
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);
}

Atronax 22.03.21 в 15:22

0

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

Atronax 22.03.21 в 15:22

0

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

Atronax 22.03.21 в 15:23

0

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

Atronax 22.03.21 в 15:23

0

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

Atronax 22.03.21 в 15:23

0

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

Atronax 22.03.21 в 15:23

0

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

Atronax 22.03.21 в 15:23

0

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

Atronax 22.03.21 в 15:23

0

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

Atronax 22.03.21 в 15:23

0

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

Atronax 22.03.21 в 15:24

0

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

Atronax 22.03.21 в 15:24

0

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

Atronax 22.03.21 в 15:24

0

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

Atronax 22.03.21 в 15:24

0

// отримуємо потрібні дані з моделі 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);

Atronax 22.03.21 в 15:25

0

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

Atronax 22.03.21 в 15:26

0

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

Atronax 22.03.21 в 15:26

0

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

Atronax 22.03.21 в 15:26

0

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

Atronax 22.03.21 в 15:26

0

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

Atronax 22.03.21 в 15:26

0

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

Atronax 22.03.21 в 15:26

0

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

Atronax 22.03.21 в 15:26

0

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);
}

Atronax 22.03.21 в 15:27

0

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

Atronax 22.03.21 в 15:28

0

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

Atronax 22.03.21 в 15:28

0

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

Atronax 22.03.21 в 15:28

0

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

Atronax 22.03.21 в 15:28

0

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

Atronax 22.03.21 в 15:28

0

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

Atronax 22.03.21 в 15:31

0

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

Atronax 22.03.21 в 15:31

0

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

Atronax 22.03.21 в 15:32

0

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

Atronax 22.03.21 в 15:32

0

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

Atronax 22.03.21 в 15:32

0

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);

Atronax 22.03.21 в 15:33

0

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

Atronax 22.03.21 в 15:33

0

// Малюємо кнопку графічного інтерфейсу на зображенні за допомогою 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;
}

Atronax 22.03.21 в 15:34

0

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

Atronax 22.03.21 в 15:35

0

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

Atronax 22.03.21 в 15:35

0

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

Atronax 22.03.21 в 15:35

0

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

Atronax 22.03.21 в 15:35

0

// Створюємо вікно графічного інтерфейсу для виводу на екран.
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;

Atronax 22.03.21 в 15:36

0

Режим пошуку

Atronax 22.03.21 в 15:36

0

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

Atronax 22.03.21 в 15:36

0

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

Atronax 22.03.21 в 15:36

0

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);
}
}

Atronax 22.03.21 в 15:37

0

Накладемо поточне попередньо оброблене обличчя у верхній центральній частині вікна таким чином:
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);

Atronax 22.03.21 в 15:37

0

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

Atronax 22.03.21 в 15:38

0

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

Atronax 22.03.21 в 15:38

0
Минутку...

← Предыдущая страница

Следующая страница →