Готовый перевод Mastering OpenCV with Practical Computer Vision Projects / Опановуюємо OpenCV через практичні проекти комп'ютерного зору: Розділ 6. Нецілісне відслідковування обличчя
Нецілісне відслідковування обличчя, тобто пошук квазі-щільного набору відповідних особливих точок на кожному кадрі відеоматеріалу, - це складне питання, для вирішення якого сучасні підходи запозичують ідеї у багатьох пов'язаних сферах, включно з комп'ютерним зором, обчислювальною геометрією, машинним навчанням та обробкою зображення. Нецілісність вказує на те, що відносні відстані між особливими точками змінюються в залежності від виразу обличчя та для різних людей, а власне алгоритм відрізняється від розпізнавання та відслідковування, які націлені лише на пошук обличчя на кожному кадрі, а не конфігурації особливих точок. Нецілісне відслідковування обличчя - це популярна тема досліджень, яка значно зросла за останні два десятиліття, але лише нещодавно підходи стали достатньо надійними, а процесори - достатню швидкість для того, щоб мати змогу створювати комерційні застосунки.
Хоча відслідковування обличчя комерційного рівня може становити занадто складну проблему та виклик навіть для досвідчених фахівців комп'ютерного зору, у цьому розділі ми побачимо, що інструмент, який надає якісні результати за певних обмежень, можна створити за допомогою звичайних математичних методів та основної функціональності бібліотеки OpenCV у сферах лінійної алгебри, обробки зображень та візуалізації. Це особливо стосується випадку, коли особа відома заздалегідь, і наявні тренувальні дані - у формі зображень та приміток. Таким чином, описані техніки відігратимуть роль корисного джерела інформації, необхідної для подальшого вдосконалення системи відслідковування облич.
Розділ присвячений наступним темам:
Огляд: короткі відомості з історії відслідковування облич.
Інструменти: огляд уживаних структур та домовленостей; сюди відносимо об'єктно-орієнтований дизайн, зберігання та представлення даних та інструменти для збору даних та роботи з відмітками.
Геометричні обмеження: аналіз інформації про різновиди геометрії обличчя на основі тренувальних даних та їх використання спрощення пошуку рішення; у цій частині також розповідається, як представити обличчя у вигляді певної лінійної форми та інтегрувати відповідні глобальні перетворення.
Детектори особливих точок обличчя: розпізнавання особливих точок для їх подальшого пошуку на зображенні, на якому маємо відслідкувати особу.
Пошук обличчя та налаштування відслідковування: як саме використати детектори з цією метою.
Відслідковування обличчя: поєднання попередньо описаних компонентів у систему через процес вирівнювання зображень; розглянемо також налаштування параметрів, за яких процес працюватиме найкраще.
Наступна блочна діаграма ілюструє взаємозв'язки між різними компонентами системи:
Примітка
Зауважте, всі описані в розділі методи відповідають парадигмі, за якої всі моделі сформовано на основі використаних даних, а не штучних налаштувань. Таким чином, кожен компонент системи включає два етапи: тренування та тестування. На першому кроці будуємо моделі з використанням наявних даних, а на другому пристосовуємо їх до нових зображень.
Огляд
Проблема нецілісного відслідковування обличчя вперше набула розголосу на початку 90-х років ХХ століття з винаходом активних моделей форм (Active Shape Models, ASM) Кута та Тейлора. Відтоді здійснено величезну кількість досліджень, які мали вирішити проблему відслідковування будь-якого обличчя з кращими результатами в порівнянні з початковим алгоритмом. Важливим кроком виявилось розширення ASM до активних моделей зовнішнього вигляду (Active Appearance Models, AAM) у 2001. Дещо пізніше, в середині 2000-х років, Бейкер та його колеги формалізували цей підхід у вигляді чіткого трактування викривлень зображень.
До важливих витків розвитку відносять також працю Бланза та Веттера, присвячену змінним 3D-моделям (3D Morphable Model, 3DMM), які, подібно до AAM, не лише враховували текстури зображень замість профілів вздовж границь об'єкта, як при ASM, а й здійснили додатковий крок, отримавши моделі з високою щільністю 3D-даних за допомогою методу лазерного сканування. Із середини до кінця 2000-х років фокус досліджень змістився від характеристик обличчя до представлення та оптимізації задачі алгоритму відслідковування. З різною ступінню успіху використано різноманітні створені спільнотою техніки машинного навчання. Нова декада знову принесла зміни, цього разу у напрямі питання поєднаності точок та стратегій постановки задач, що гарантують глобальні рішення.
Не зважаючи на інтенсивні дослідження проблеми відслідковування обличчя, досить мало комерційних застосунків використовували їх результати. Ентузіасти спочатку також не переймалися цією темою, хоча у вільному доступі й існували пакети коду для деяких поширених підходів. Але за останні два роки інтерес до потенціального застосування відслідковування обличчя та побудови продуктів комерційного рівня почав відновлюватися.
Інструменти
Перш ніж проблема поглине нас цілковито, розглянемо теоретичні задачі та домовленості поширених методів відслідковування обличчя. Подальший матеріал розділу, звісно, матиме справу з цими питаннями. Хоча зацікавлений читач може пропустити частину та продовжити знайомство з геометричних обмежень.
Об'єктно-орієнтований дизайн
Як і процеси пошуку та розпізнавання облич, відслідковування складається з двох компонентів: даних та алгоритмів. Алгоритми здійснюють типові операції над вхідними (онлайн) даними шляхом пошуку відповідностей серед попередньо збережених (офлайн). Тобто, об'єктно-орієнтований дизайн, який поєднує алгоритми з необхідними даними, є досить зручним рішенням.
В бібліотеці OpenCV v2.x з'явився зручний клас для зберігання файлів у форматі XML/YAML, які значно спрощують задачу організації офлайн даних для використання в алгоритмамх. Щоб врахувати цю можливість, всі класи розділу матимуть функції для серіалізації даних. Що це значитиме, покажемо на прикладі уявного класу foo:
#include <opencv2/opencv.hpp>
using namespace cv;
class foo{
public:
Mat a;
type_b b;
void write(FileStorage &fs) const{
assert(fs.isOpened());
fs << "{" << "a" << a << "b" << b << "}";
}
void read(const FileNode& node){
assert(node.type() == FileNode::MAP);
node["a"] >> a; node["b"] >> b;
}
};
У вище наведеному фрагменті коду Mat - це клас матриці OpenCV, а type_b - це уявний клас з можливістю серіалізації даних, яку надають реалізовані функції зчитування та запису. FileStorage підтримує два типи структур, які можна зберегти та відновити. Для простоти викладок у цьому розділі всі класи оперуватимуть відображеннями, тобто кожне збережене значення створює об'єкт FileNode типу FileNode::MAP. Для цього маємо встановити унікальний ключ для кожного елемента. Хоча його вибір є довільним, для зручності в якості відмітки використаємо назву змінної. Як показано вище, функції зчитування та запису мають досить простий вигляд, використані лише оператори потоку ("<<" та ">>") для вставки та видалення даних з об'єкта FileStorage. Варто зауважити, більшість класів OpenCV мають реалізацію цих функцій, тому можна легко зберегти їх для подальшого використання.
Крім визначення функції для серіалізації, мусимо також створити дві додаткові функції у класі FileStorage з таким кодом:
void write(FileStorage& fs, const string&, const foo& x){
x.write(fs);
}
void read(const FileNode& node, foo& x,const foo& default){
if(node.empty())x = d; else x.read(node);
}
Оскільки можливості двох функцій не змінюються для усіх класів частини, запрограмуємо відповідні шаблонні версії та подамо їх визначення у частині файла заголовків ft.hpp, що відповідає цьому розділу. Нарешті, щоб мати можливість легко зберігати та відновлювати класи користувачів, які користуються серіалізацією даних, відповідні шаблонні функції також реалізовані у файлі заголовків:
template <class T>
T load_ft(const char* fname){
T x; FileStorage f(fname,FileStorage::READ);
f["ft object"] >> x; f.release(); return x;
}
template<class T>
void save_ft(const char* fname,const T& x){
FileStorage f(fname,FileStorage::WRITE);
f << "ft object" << x; f.release();
}
Пов'язана з об'єктом відмітка завжди залишається незмінною. За допомогою визначених вище функцій можна без жодних зусиль зберігати та завантажувати дані класу. Це легко бачити в наступному фрагменті коду:
#include "opencv_hotshots/ft/ft.hpp"
#include "foo.hpp"
int main(){
...
foo A; save_ft<foo>("foo.xml",A);
...
foo B = load_ft<foo>("foo.xml");
...
}
Зауважте, що розширення.xml вказує на файл даних, форматований за правилами XML. Будь-яке інше значення встановлює більш зрозумілий для людини тип YAML.
Колекція даних: відмітки зображень та відеокадрів
Сучасні техніки відслідковування обличчя практично повністю засновані на даних, тобто алгоритми, що використовуються для розпізнавання положення особливих точок на зображеннях, діють на основі моделей зовнішнього вигляду та геометричних залежностей між їх відносними позиціями в наборі прикладів. Чим більший об'єм набору, тим ефективніше діють алгоритми, адже вони знатимуть про різноманіття варіантів облич. Тому на першому кроці побудови алгоритму відслідковування обличчя створимо інструмент для позначень, за допомогою якого користувач зможе вказати положення бажаних особливих точок на кожному зображенні прикладу.
Тренування типів даних
Дані для тренування алгоритмів відслідковування обличчя звичайно складаються з чотирьох компонентів:
Зображення. Це колекція звичайних зображень або кадрів відео, на яких можна побачити обличчя цілком. Щоб отримати кращі результати, її можна розділити на категорії залежно від певних характеристик (наприклад особи, освітлення, відстані до камери, використаного пристрою для зйомки), для яких надалі налаштуємо відслідковувач. Дуже важливо, аби колекція враховувала також можливі пози голови та вирази обличчя, необхідні для застосунку.
Анотації. Цей компонент стосується ручних позначень на зображеннях, які відповідають кожній особливій точці обличчя, що має бути відслідковане. Чим більша кількість особливих точок обличчя, тим надійніші результати відслідковування, адже алгоритм зможе використати додаткові дані для уточнення. Час обчислення поширених алгоритмів відслідковування зазвичай зростає лінійно, залежно від числа особливих точок.
Індекси симетрії. Тут зберігаються індекси для кожної особливої точки обличчя, що відповідає симетричному фрагменту. Ці дані можна використати для віддзеркалення тренувальних зображень, подвоївши розмір набору та розмістивши дані симетрично відносно осі y.
Індекси спорідненості. Елемент включає набір пар індексів анотацій, що визначають семантичну інтерпретацію особливих точок обличчя. Ці зв'язки корисні для візуалізації результатів відслідковування.
Вказані чотири компоненти зображені далі, на якому зліва направо можна побачити необроблене зображення, позначення особливих фрагментів, закодовані кольором точки подвійної симетрії, віддзеркалений кадр, спорідненість особливих точок.
Щоб зручно взаємодіяти з ними, реалізовано можливості зберігання та зчитування. Клас CvMLData модуля ml бібліотеки OpenCV має функціональність для обробки звичайних даних, які часто використовується для вирішення питань машинного навчання. Але йому не вистачає функцій для роботи з інформацією відслідковування. Тому у цьому розділі скористаємось класом ft_data, що оголошений у файлі заголовку ft_data.hpp, розробленим з урахуванням особливостей даних відслідковування обличчя. Майте на увазі, всі елементи класу визначені як публічні змінні:
class ft_data{
public:
vector<int> symmetry;
vector<Vec2i> connections;
vector<string> imnames;
vector<vector<Point2f> > points;
…
}
Типи Vec2i та Point2f - це класи OpenCV, що позначають вектори двох цілих чисел та двовимірні координати точки відповідно. Вектор симетрії має стільки ж компонентів, скільки існує особливих точок на обличчі (визначених користувачем). Кожен зі зв'язків визначає пару індексів для поєднаних елементів. Оскільки тренувальний набір потенційно може були досить великим, замість зображень клас зберігає назви файлів у змінній imnames (для правильної роботи програми вони мають знаходитися за відносним шляхом, що відповідає значенням). Нарешті, для кожного тренувального кадру колекція позицій відповідних точок зберігається у вигляді векторів координат, у змінній класу points.
Клас ft_data реалізує декілька зручних методів доступу до даних. Щоб отримати зображення з набору, можна скористатися функцією get_image, яка завантажить визначений індекс idx та за вибором користувача віддзеркалить результат вздовж осі y:
Mat ft_data::get_image(
const int idx, // індекс зображення для завантаження файлу
const int flag){ //0 = сір.,1 = сір. + віддз. ,2 = кольор. ,3 = кольор. + віддз.
if((idx < 0) || (idx >= (int)imnames.size()))return Mat();
Mat img,im;
if(flag < 2)img = imread(imnames[idx],0);
else img = imread(imnames[idx],1);
if(flag % 2 != 0)flip(img,im,1); else im = img;
return im;
}
Значення параметра flag (0,1), що передається функції imread бібліотеки OpenCV, визначає, чи приклад завантажений як 3-канальне кольорове зображення чи одноканальне у відтінках сірого. Відмітка, що передається у функцію flip, позначає віддзеркалення відносно осі y.
Отримати доступ до набору точок, що відповідає зображенню з певним індексом, можна за допомогою функції get_points, що повертає вектор координат з можливістю віддзеркалити індекси:
vector<Point2f> ft_data::get_points(
const int idx, // індекс зображення, що відповідає точкам
const bool flipped){ // чи віддзеркалене зображення відносно осі y?
if((idx < 0) || (idx >= (int)imnames.size()))
return vector<Point2f>();
vector<Point2f> p = points[idx];
if(flipped){
Mat im = this->get_image(idx,0); int n = p.size();
vector<Point2f> q(n);
for(int i = 0; i < n; i++){
q[i].x = im.cols-1-p[symmetry[i]].x;
q[i].y = p[symmetry[i]].y;
}return q;
}else return p;
}
Якщо відмітка віддзеркалення визначена, вище зазначена функція викликає get_image для знаходження ширини зображення, аби мати можливість коректно відобразити координати особливих точок обличчя. Можна діяти й інакше - просто передати відповідне значення в якості параметра. Функція також ілюструє інструмент для симетричного відображення змінних. Віддзеркалене положення особливої точки з певним індексом - це позиція, визначена у симетричній змінній, до координати x якої застосовано переворот та зміщення.
Обидві функції get_image та get_points повертають пусті структури, якщо визначений індекс знаходиться за межами набору даних. Також можлива ситуація, за якої не всі зображення колекції матимуть відповідні позначення. Алгоритми відслідковування обличчя можуть врахувати обробку неіснуючих даних, такі реалізації доволі часто використовуються й знаходяться за межами матеріалу цього розділу. У нашому ж випадку клас ft_data реалізує функцію для видалення з колекції примірників без позначень таким чином:
void ft_data::rm_incomplete_samples(){
int n = points[0].size(),N = points.size();
for(int i = 1; i < N; i++)n = max(n,int(points[i].size()));
for(int i = 0; i < int(points.size()); i++){
if(int(points[i].size()) != n){
points.erase(points.begin()+i);
imnames.erase(imnames.begin()+i); i--;
}else{
int j = 0;
for(; j < n; j++){
if((points[i][j].x <= 0) ||
(points[i][j].y <= 0))break;
}
if(j < n){
points.erase(points.begin()+i);
imnames.erase(imnames.begin()+i); i--;
}
}
}
}
Об'єкт з найбільшою кількістю відміток вважається канонічним. Усі екземпляри, які мають набір точок з меншою кількістю, видаляються з колекції за допомогою методу вектора erase. Також майте на увазі, що точки з координатами (x, y), менші за одиницю, вважаються відсутніми на відповідному зображенні (через перекриття, погану видимість чи невизначеність).
Клас ft_data реалізує функції серіалізації, тому його дані можна легко зберегти або відновити. Наприклад, збереження набору можна здійснити настільки просто:
ft_data D; // інстанціюємо структуру даних
… // заповнюємо
save_ft<ft_data>("mydata.xml",D); // зберігаємо
Для візуалізації набору даних клас ft_data пропонує декілька функцій. Їх застосування ілюструє файл visualize_annotations.cpp. Проста програма завантажує дані позначень, що зберігаються у файлі, визначеному командною стрічкою, видаляє неповні примірники та виводить на екран тренувальні зображення з відповідними відмітками, симетрією та встановленими зв'язками особливих точок. Також використано декілька важливих особливостей модуля highgui бібліотеки OpenCV. Доволі рудиментарні та недостатні для складних інтерфейсів користувача, можливості модуля досить корисні для завантаження та візуалізації даних, а також для алгоритмічного виводу в застосунках комп'ютерного зору. Це, можливо, одна з найважливіших відмінностей OpenCV від інших бібліотек комп'ютерного зору.
Інструменти для анотації
Щоб полегшити застосування анотацій у подальшому матеріалі розділу, скористаємось найпростішим інструментом з файлу annotate.cpp. Його вхідним параметром є відеопотік з файлу або камери. Процедура використання складається з чотирьох кроків:
1. Захоплення зображень. На цьому етапі потік зображень виводиться на екран, а користувач, натискаючи клавішу S, обирає ті з них, які потрібно позначити. Для збільшення ефективності краще зупинитися на особливих точках, що сприяють максимальному зростанню варіативності поведінки обличчя, яку матиме відслідкувати система.
2. Анотація першого зображення. На другому кроці користувач розглядає приклад, обраний на попередньому етапі. Потім він відмічає на ньому певний набір фрагментів, які мають бути знайдені.
3. Анотація спорідненості. На третьому етапі потрібно визначити структуру споріднених точок для кращої візуалізації форми. У цьому випадку користувачу показано те ж зображення, що й на попередньому етапі, але нинішнє завдання стосується вибору набору пар точок, однієї за одною, щоб побудувати на моделі обличчя структуру спорідненості.
4. Анотація симетрії. На цьому кроці, все ще на тому ж зображенні, користувач відмічає пари точок, що характеризують подвійну симетрію.
5. Анотація решти зображень. Це останній етап, процедура схожа на другий, за винятком того, що користувач може перемикатися між зображеннями набору та анотувати кожне окремо.
Зацікавлений читач може вдосконалити інструмент, зробивши його зручнішим для використання або навіть інтегрувати процедуру навчання, за якою модель відслідковування оновлюємо після кожного нового анотованого зображення, а потім використовуємо результати для налаштування точок, щоб зменшити кількість зайвих позначень.
Хоча деякі публічно доступні набори даних можна використати у коді цього розділу (перегляньте опис прикладу у попередній частині), інструмент допоможе в побудові моделей для відслідковування облич певних осіб, які зазвичай ефективніші, ніж загальні алгоритми, незалежні від людини.
Заздалегідь анотовані дані (набір даних MUCT)
Одним з ключових факторів розробки систем відслідковування обличчя є тривалий і нудний процес ручного позначення значної кількості точок на кожному зображенні з чималої колекції. Щоб полегшити процес для цілей цього розділу, можна завантажити доступний набір даних MUCT з ресурсу http://www/milbo.org/muct.
Набір даних складається з 3755 зображень облич, що анотовані 76 відмітками. Суб'єкти набору відрізняються за віком та етносом, фотографії зняті у різних умовах освітлення та з різними позами голови.
Щоб мати змогу користуватися даними MUCT у розділі, виконаємо такі кроки:
1. Завантажимо набір зображень. Для цього завантажимо файли muct-a-jpg-v1.tar.gz to muct-e-jpg-v1.tar.gz та розпакуємо їх. Це згенерує нову теку, у якій зображення будуть збережені.
2. Отримаємо анотації. На цьому кроці маємо здобути файл з анотаціями muct-landmarks-v1.tar.gz. Завантажимо та розпакуємо його у ту ж теку, що й зображення з попереднього етапу.
3. Визначимо зв'язки та симетрію за допомогою інструмента анотації. Для цього виконаємо команду ./annotate -m $mdir -d $odir, де $mdir вказує на теку, де має бути збережений набір даних MUCT, а $odir - на теку, де знаходитиметься файл анотації у форматі yaml, у якому зберігаються дані об'єкта ft_data.
Підказка
Використання набору даних MUCT дозволяє швидше перейти до розгляду функціональності коду відслідковування обличчя, що описане далі у цьому розділі.
Геометричні обмеження
У відслідковуванні обличчя геометрія вказує на просторову конфігурацію попередньо визначеного набору точок, що відповідають фізично стабільному положенню людського обличчя (наприклад кути ока, кінчик носа та дуги брів). Особливості їх вибору цілком залежать від застосунку, деякі потребують щільний набір з понад 100 точок, а інші - дещо рідші варіанти. Варто зауважити, ефективність алгоритмів відслідковування обличчя зазвичай покращується з підвищенням кількості точок, оскільки окремі виміри можуть допомогти відшукати інші шляхом відносної просторової залежності. Наприклад, знаючи положення кута ока, ми з вищою вірогідністю можемо спрогнозувати, де знаходиться ніс.
Разом з тим, для кількості точок існують певні межі, після якої їх зростання не підвищує ефективність, зазвичай продуктивність перестає змінюватися приблизно біля ста обраних фрагментів. Більше того, зростання розміру набору для опису обличчя тягне за собою лінійне підвищення складності обчислень. Тому для застосунків із суворими обмеженнями ресурсів краще використати менше точок. До того ж, швидше відслідковування часто призводить до точніших результатів у випадку онлайн налаштувань.
Чому це відбувається? Коли відкидаються кадри, отриманий рух між кадрами пришвидшується, при цьому алгоритм оптимізації, що має знайти конфігурацію обличчя на кожному зображенні, має відшукати більшу кількість можливих конфігурацій особливих точок; процес може сформувати невірні результати, якщо зміщення між кадрами надто велике. Тому, хоча й існують загальні поради щодо правильного вибору особливих точок обличчя для оптимізації продуктивність, ця процедура залежить від конкретних цілей застосунку.
Геометрія обличчя часто параметризується композицією двох елементів: глобальної (цілісної) трансформації та локальної (нецілісної) деформації. Перший компонент враховує загальне розміщення обличчя на зображенні, яке може змінюватися без обмежень (тобто, обличчя може з'явитися будь-де на кадрі). До глобальної транформації відносять положення (x, y), поворот та розмір обличчя. Другий, з іншого боку, вказує на різницю між формами облич різних осіб та з різними виразами. На відміну від глобальних змін, локальні часто більш обмежені через структуровану конфігурацію особливих точок обличчя. Глобальні трансформації - це звичайні функції з двовимірними координатами, які можна застосувати до будь-якого типу об'єкта, а дані про локальні деформації залежать від конкретного об'єкта та мають отримуватися на основі тренувального набору даних.
У цьому розділі опишемо побудову геометричної структури обличчя, тобто моделі форми. Залежно від застосунку, вона може зберігати вирази обличчя однієї особи, різноманітні його форми серед населення чи комбінацію двох елементів. Така модель реалізована у класі shape_model, що визначений у файлі shape_model.hpp та реалізований у shape_model.cpp. Наступний фрагмент коду - це частина заголовку класу shape_model, що описує його базову функціональність:
class shape_model{ // двовимірна лінійна модель форми
public:
Mat p; // вектор параметрів (kx1) CV_32F
Mat V; // лінійний підпростір (2nxk) CV_32F
Mat e; // вектор дисперсії (kx1) CV_32F
Mat C; // спорідненість (cx2) CV_32S
...
void calc_params(
const vector<Point2f> &pts, // точки для пошуку параметрів
const Mat &weight = Mat(), // вага (nx1) CV_32F
const float c_factor = 3.0); // множник стискання
...
vector<Point2f> // визначена параметрами форма
calc_shape();
...
void train(
const vector<vector<Point2f> > &p, // форми з N прикладів
const vector<Vec2i> &con = vector<Vec2i>(),// спорідненість
const float frac = 0.95, // бажаний відсоток варіативності
const int kmax = 10); // бажана максимальна кількість режимів
...
}
Модель, що описує варіанти форм обличчя, закодована матрицею підпростору V та вектором дисперсії e. Вектор параметрів p зберігає спосіб кодування форми відповідно до моделі. Матриця спорідненості C також зберігається в цьому класі, оскільки його метою є лише візуалізація екземплярів. Найцікавіші три методи - це calc_params, calc_shape та train. Функція calc_params проєктує набір точок на простір вірогідних форм обличчя. За вибором користувача вона може також вирахувати окремі значення ваги впевненості для кожної точки, що має бути спроєктованою. Друга функція, calc_shape, генерує набір точок шляхом декодування вектору параметрів p на основі моделі обличчя, закодованої в структурах даних V та e. Функція тренування призначена для навчання моделі на основі колекції зображень різних форм обличчя, кожне з яких складається з однакової кількості точок. Параметри frac та kmax - це змінні процедури тренування, які можна визначати залежно від даних.
З функціональністю даного класу познайомимось далі, почнемо з аналізу Прокруста, методу для глобального реєстрування набору точок та лінійної моделі для представлення локальних деформацій. Код файлів train_shape_model.cpp та visualize_shape_model.cpp призначений для тренування та візуалізації моделей форм відповідно. Як їх використати, дізнаємось в кінці цієї частини.
Аналіз Прокруста
Щоб побудувати модель деформації форм обличчя, спершу маємо обробити анотовані дані, щоб видалити компоненти, що відносяться до глобального руху. У випадку моделювання геометрії в двовимірному просторі цей рух часто представлений як перетворення подібності, яке включає масштаб, поворот на площині та перенесення. На наступному малюнку можна побачити можливі типи руху для перетворень подібності. Процес видалення глобальних рухів для колекції точок називають аналізом Прокруста.
З точки зору математики мета аналізу Прокруста - одночасно знайти канонічну форму та перетворення подібності для кожного екземпляра даних, яке вирівнює його з канонічною. У цьому разі, вирівнювання вимірюється найменшим квадратом відстані між кожною зміненою та канонічною формами. Поступова процедура виконання цієї задачі реалізоване у класі shape_model таким чином:
#define fl at<float>
Mat shape_model::procrustes(
const Mat &X, // дані форм у вигляді колонок
const int itol, // максимальне число ітерацій
const float ftol) // показник збіжності
{
int N = X.cols,n = X.rows/2; Mat Co,P = X.clone();// копіюємо
for(int i = 0; i < N; i++){
Mat p = P.col(i); // i-та форма
float mx = 0,my = 0; // обчислюємо центр мас
for(int j = 0; j < n; j++){ // для x та y окремо
mx += p.fl(2*j); my += p.fl(2*j+1);
}
mx /= n; my /= n;
for(int j = 0; j < n; j++){ // видаляємо центр мас
p.fl(2*j) -= mx; p.fl(2*j+1) -= my;
}
}
for(int iter = 0; iter < itol; iter++){
Mat C = P*Mat::ones(N,1,CV_32F)/N; // знаходимо нормовані значення
normalize(C,C); // канонічна форма
if(iter > 0){if(norm(C,Co) < ftol)break;} // збігається?
Co = C.clone(); // зберігаємо поточний результат
for(int i = 0; i < N; i++){
Mat R = this->rot_scale_align(P.col(i),C);
for(int j = 0; j < n; j++){ // застосуємо перетворення подібності
float x = P.fl(2*j,i),y = P.fl(2*j+1,i);
P.fl(2*j ,i) = R.fl(0,0)*x + R.fl(0,1)*y;
P.fl(2*j+1,i) = R.fl(1,0)*x + R.fl(1,1)*y;
}
}
}return P; // повертаємо вирівнені форми
}
Спочатку алгоритм видаляє центри мас з кожного екземпляра даних, а потім активує ітеративну процедуру, що змінюється для кожної канонічної форми таким чином, щоб нормоване середнє значення, повороти та масштабування відповідали їй з максимальною точністю. Нормування необхідне, щоб відрегулювати масштаб проблеми та запобігти можливому видаленню усіх форм. Вибір значення масштабу довільний, у даному разі довжина вектора канонічної форми встановлюється на рівні 1.0. Це поведінка функції нормування OpenCV за замовчуванням. Обчислення повороту на площині та масштабування, які вирівнюють кожен екземпляр форми з поточною оцінкою канонічної з найвищою точністю здійснюється за рахунок функції rot_scale_align:
Mat shape_model::rot_scale_align(
const Mat &src, // [x1;y1;...;xn;yn] - вектор вхідних форм
const Mat &dst) // вихідні форми
{
// конструюємо лінійну систему рівнянь
int n = src.rows/2; float a=0,b=0,d=0;
for(int i = 0; i < n; i++){
d+= src.fl(2*i)*src.fl(2*i )+src.fl(2*i+1)*src.fl(2*i+1);
a+= src.fl(2*i)*dst.fl(2*i )+src.fl(2*i+1)*dst.fl(2*i+1);
b+= src.fl(2*i)*dst.fl(2*i+1)-src.fl(2*i+1)*dst.fl(2*i );
}
a /= d; b /= d;// знаходимо її розв'язки
return (Mat_<float>(2,2) << a,-b,b,a);
}
Функція шукає мінімальне значення серед квадратів різниць між перевернутою та канонічною формами. Математично це можна описати так:
У цьому випадку для вирішення проблеми найменших квадратів застосовано закриту форму, зображену на наступному малюнку, праворуч від рівняння. Замість пошуку масштабу та повороту на площині, які розміщено нелінійно в масштабованій двовимірній матриці повороту, ми шукаємо розв'язок відносно змінних (a, b). Ці параметри пов'язані з масштабом та матрицею поворотів таким чином:
Візуалізація ефектів аналізу Прокруста для необроблених анотованих даних ілюстрована далі. Особливі фрагменти обличчя позначені унікальними кольорами. Після нормування компоненти перенесення структура обличчя стає очевидною, точки скупчуються поряд з їх середніми положеннями. Ітеративна процедура масштабування та нормування повороту сприяє більш компактному скупченню, а розподіл особливих точок краще описуватиме зміни, спричинені деформацією обличчя. Саме їх ми намагатимемось змоделювати у наступній частині. Таким чином, аналіз Прокруста має лише обробити дані так, щоб мати змогу знайти кращі моделі локальної деформації для обличчя, яке вивчає алгоритм.
Лінійні моделі форм
Моделювання деформацій обличчя призначене для пошуку компактного параметричного представлення залежності форми обличчя від особи та виразу. Існує багато способів різної складності досягти цієї мети. Найпростіший серед них - скористатися лінійнім представленням геометрії обличчя. Не зважаючи на простоту, процес досить точно охоплює простір деформацій, особливо якщо зображення обличчя зняті переважно з фронтальним видом. До позитивних сторін також відносять те, що зміна параметрів представлення у цьому разі - винятково легка та дешева операція, на відміну від інших, нелінійних способів. Це відіграє досить важливу роль в подальшому, для обмеження процедури пошуку в процесі відслідковування обличчя.
Головну ідею лінійного моделювання проілюстровано на наступному малюнку. Форма обличчя, що складається з N особливих точок, моделюється у вигляді єдиної в 2N-вимірному просторі. Мета вище зазначеного процесу полягає в пошуку гіперплощини низької розмірності, на якій лежать усі точки форми обличчя (позначені зеленим кольором на зображенні). Оскільки вона є лише частиною 2N-вимірного простору, досить часто її називають підпростором. Чим менша розмірність, тим компактніше представлення обличчя та тим більші обмеження накладаються на процедуру відслідковування.
Завдяки цьому підвищується ефективність алгоритму.
Однак обирати розмірність підпростору варто дуже уважно, щоб місця вистачило для всіх облич, при цьому інші об'єкти не мають потрапити в його межі (червоні точки на зображенні). Якщо моделюються дані однієї людини, варто врахувати, що у цьому випадку підпростір з можливими варіантами обличчя часто набагато компактніший за той, в якому моделюються обличчя декількох особ. Саме тому специфіковані застосунки для відслідковування працюють ефективніше, ніж загальні.
Процедура пошуку найкращого підпростору низької розмірності, що включає набір даних, називається методом головних компонент (Principal Component Analysis, PCA). Бібліотека OpenCV реалізує клас для пошуку PCA, але він потребує визначення кількості збережених розмірностей підпростору. Оскільки це важко зробити апріорі, поширеною евристикою є вибір на основі частини від загальної кількості обраних варіацій. У функції shape_model::train метод головних компонент реалізований таким чином:
SVD svd(dY*dY.t());
int m = min(min(kmax,N-1),n-1);
float vsum = 0; for(int i = 0; i < m; i++)vsum += svd.w.fl(i);
float v = 0; int k = 0;
for(k = 0; k < m; k++){
v += svd.w.fl(k); if(v/vsum >= frac){k++; break;}
}
if(k > m)k = m;
Mat D = svd.u(Rect(0,0,k,2*n));
Кожна колонка змінної dY позначає вирівнені за методом Прокруста форми з видаленим середнім значенням. До матриці розбіжності даних форми, тобто dY.t()*dY), ефективно застосовано декомпозицію єдиного значення (Singular Value Decomposition, SVD). Змінна w екземпляра класу SVD бібліотеки OpenCV у цьому випадку зберігає дисперсію в головних напрямках варіативності, від найбільшого до найменшого. Загальновживаний спосіб визначити розмірність підпростору - обрати найменший набір тих із них, що збереже частину frac загальної енергії даних, представлену елементами svd.w.
Оскільки вхідні значення упорядковані від найбільшого до найменшого, достатньо обрати підпростори, просто оцінивши енергію у перших k напрямках варіативності. При цьому напрямки зберігаються у змінній u екземпляра класу SVD. Компоненти svd.w та svd.u зазвичай називають власним спектром та власними векторами відповідно. Їх візуалізацію зображено далі:
Примітка
Власний спектр має властивість швидко скорочуватися, тому справедливо зазначити, що більшість варіацій даних можна змоделювати у підпростірі низької розмірності.
Комбіноване локально-глобальне представлення
Форма обличчя на зображенні створюється шляхом композиції локальної деформації та глобального перетворення. З точки зору математики таку параметризацію доволі складно здійснити, адже композиція формує нелінійну функцію з розв'язками, які не можна представити в закритій формі. Щоб обійти цю проблему, глобальні перетворення часто моделюють у вигляді лінійного підпростору та додають результат у підпростір деформацій. Що ж до фіксованих форм, можна застосувати перетворення подібності з таким підпростором:
Функція calc_rigid_basis класу shape_model генерує цей підпростір на основі усередненої канонічної форми згідно аналізу Прокруста. Крім власне конструювання підпростору з вище зазначених даних, кожна колонка матриці нормується до одиничної довжини. У функції shape_model::train зазначене раніше змінна dY обчислюється за допомогою проєкції компонентів даних, що зберігають глобальний рух, таким чином:
Mat R = this->calc_rigid_basis(Y); // обчислюємо потрібний підпростір
Mat P = R.t()*Y; Mat dY = Y – R*P; // видаляємо глобальний рух
Варто зауважити, що проєкція реалізована звичайним добутком матриць. Це можливо завдяки тому, що колонки підпростору глобального руху мають нормовану довжину. У такому випадку не змінюється навіть зайняте моделлю місце, а це значить, що R.t()*R дорівнює одиничній матриці.
Оскільки напрямки варіативності, що походять від глобальних перетворень, видалені з даних до початку навчання моделі деформації, підпростір результату є ортогональним до підпростору трансформацій. Тобто, об'єднавши їх, отримаємо комбіноване локально-глобальне лінійне представлення форм обличчя, яке на додаток є ортонормованим. Об'єднання здійснюється шляхом присвоєння двох матриць підпросторів відповідним підматрицям комбінованої матриці підпростору за допомогою механізму пошуку ROI, що реалізований у класі Mat бібліотеки OpenCV таким чином:
V.create(2*n,4+k,CV_32F); // комбінований підпростір
Mat Vr = V(Rect(0,0,4,2*n)); R.copyTo(Vr); // підпростір глобальних рухів
Mat Vd = V(Rect(4,0,k,2*n)); D.copyTo(Vd); // підпростір деформацій
Ортонормованість моделі результату означає, що параметри форми можна легко вирахувати, наприклад для функції shape_model::calc_params це відбувається так:
p = V.t()*s;
У цьому випадку s - це вектор форми обличчя, а p - координати у підпросторі, що її визначають. Насамкінець, у лінійному моделюванні існує ще одна досить важлива проблема, - як обмежити координати підпростору таким чином, щоб згенеровані форми залишалися дійсними. Наступний малюнок ілюструє форми обличчя в межах підпростору для зростаючого значення координат в одному з напрямів варіативності, шляхом поступового підвищення чотирьох основних відхилень. Для малих значень форма залишається схожою на обличчя, і результат погіршується, якщо вони стають надто великими.
Легкий спосіб запобігти подібній деформації - вмістити значення координат підпростору в певні межі, визначені набором даних. Поширеною практикою є обмеження з ±3 основними відхиленнями даних, які врахують 99.7 відсотків варіацій. Стиснені значення обраховуються функцією shape_model::train після знаходження підпростору, так:
Mat Q = V.t()*X; // проєтуємо дані на підпростір
for(int i = 0; i < N; i++){ // нормуємо координати з урахуванням масштабу
float v = Q.fl(0,i); Mat q = Q.col(i); q /= v;
}
e.create(4+k,1,CV_32F); multiply(Q,Q,Q);
for(int i = 0; i < 4+k; i++){
if(i < 4)e.fl(i) = -1; // не стискаємо коефіцієнти глобального руху
else e.fl(i) = Q.row(i).dot(Mat::ones(1,N,CV_32F))/(N-1);
}
Дисперсія обчислюється на основі значення Q підпростору після його нормування з урахуванням координати першої розмірності (тобто масштабу). Це не дозволяє примірникам даних з відносно великим масштабом домінувати у визначенні результату. Зверніть увагу, дисперсії координат глобального підпростору (тобто перших 4 колонкам V) присвоюються негативні значення. Функція стискання shape_model::clamp перевіряє, чи є дисперсія у певному напрямі негативною, і виконує подальші дії, якщо це не так:
void shape_model::clamp(
const float c){ // коєфіцієнт стискання, відсоток стандартного відхилення
double scale = p.fl(0); // отримуємо масштаб
for(int i = 0; i < e.rows; i++){
if(e.fl(i) < 0)continue; // ігноруємо компоненти глобального руху
float v = c*sqrt(e.fl(i)); // c*стандартні відхилення
if(fabs(p.fl(i)/scale) > v){ // зберігаємо знак координати
if(p.fl(i) > 0)p.fl(i) = v*scale; // додатній поріг
else p.fl(i) = -v*scale; // від'ємний поріг
}
}
}
Причиною цьому є те, що тренувальні дані часто зняті за штучних налаштувань, наприклад обличчя зняте з фронтального вигляду, відцентроване на зображенні визначеного масштабу. Стискання компонентів глобального руху моделі форми у подібних умовах, встановлених для тренувального набору, має забагато обмежень. До того ж, оскільки дисперсія кожної деформованої координати обраховується на однові кадру з нормованим значенням масштабу, його можна застосувати до координат у процесі стискання:
Тренування та візуалізація
Приклад тренування моделі форми на основі анотованих даних можна знайти у файлі train_shape_model.cpp. Якщо вказати шлях до даних як значення аргументу argv[1] командної стрічки, алгоритм тренування завантажить їх у пам'ять та видалить неповні примірники таким чином:
ft_data data = load_ft<ft_data>(argv[1]);
data.rm_incomplete_samples();
Анотації до кожного прикладу та їх віддзеркалені варіанти, якщо такі є, зберігаються у векторі до передачі в тренувальну функцію:
vector<vector<Point2f> > points;
for(int i = 0; i < int(data.points.size()); i++){
points.push_back(data.get_points(i,false));
if(mirror)points.push_back(data.get_points(i,true));
}
Потім активується процес навчання моделі форми шляхом виклику функції shape_model::train:
shape_model smodel; smodel.train(points,data.connections,frac,kmax);
Значення frac (відсоток варіацій для збереження) та kmax (максимальна кількість власних векторів) можна налаштувати як відповідні параметри командної стрічки, хоча відповідні значення за замовчуванням, 0.95 та 20, працюють добре у більшості випадків. Нарешті, за допомогою аргумента argv[2], що вказує на шлях до натренованої моделі форм, зберігання можна здійснити єдиним викликом функції:
save_ft(argv[2],smodel);
Простота цього етапу є результатом визначення функції серіалізації даних у класі shape_model.
Програма visualize_shape_model.cpp призначена для візуалізації натренованої моделі форм. Вона зображає вивчені локальні деформації у кожному напрямі по черзі. На першому етапі модель завантажується в пам'ять:
shape_model smodel = load_ft<shape_model>(argv[1]);
Параметри глобальних перетворень, які розміщують модель в центрі вікна, можна знайти так:
int n = smodel.V.rows/2;
float scale = calc_scale(smodel.V.col(0),200);
float tranx =
n*150.0/smodel.V.col(2).dot(Mat::ones(2*n,1,CV_32F));
float trany =
n*150.0/smodel.V.col(3).dot(Mat::ones(2*n,1,CV_32F));
У цьому разі функція calc_scale обчислює коефіцієнт масштабування, який генерує форми обличчя з шириною 200 пікселів. Компоненти перенесення знаходяться на основі тих значень, що здійснюють переміщення на 150 одиниць (тобто модель відцентрована на вікні з розмірами 300x300 пікселів).
Примітка
Перша колонка shape_model::V відповідає масштабуванню, а третя та четверта - елементам переміщення x та y відповідно.
Далі генерується траєкторія значень параметрів, яка починається з нуля, рухається до додатнього, а потім до від'ємного екстремуму, і знову до нуля:
vector<float> val;
for(int i = 0; i < 50; i++)val.push_back(float(i)/50);
for(int i = 0; i < 50; i++)val.push_back(float(50-i)/50);
for(int i = 0; i < 50; i++)val.push_back(-float(i)/50);
for(int i = 0; i < 50; i++)val.push_back(-float(50-i)/50);
Кожна фаза складається з п'ятидесяти змін значень. Потім траєкторія використовується для анімації моделі обличчя та малювання результатів на вікні:
Mat img(300,300,CV_8UC3); namedWindow("модель форми");
while(1){
for(int k = 4; k < smodel.V.cols; k++){
for(int j = 0; j < int(val.size()); j++){
Mat p = Mat::zeros(smodel.V.cols,1,CV_32F);
p.at<float>(0) = scale;
p.at<float>(2) = tranx;
p.at<float>(3) = trany;
p.at<float>(k) = scale*val[j]*3.0*
sqrt(smodel.e.at<float>(k));
p.copyTo(smodel.p); img = Scalar::all(255);
vector<Point2f> q = smodel.calc_shape();
draw_shape(img,q,smodel.C);
imshow("модель форми",img);
if(waitKey(10) == 'q')return 0;
}
}
}
Примітка
Компонентам глобального руху (ті, що відповідають першим чотирьом колонкам shape_model::V) завжди присвоюються заздалегідь вирахувані значення, щоб мати змогу розмістити обличчя в центрі вікна.
Детектори особливих точок обличчя
Розпізнавання обличчя не надто відрізняється від розпізнавання будь-яких інших об'єктів. Бібліотека OpenCV пропонує набір загальних функцій для побудови детекторів довільних особливих точок, серед яких найвідоміші каскадні алгоритми Хаара. До реалізацій останніх і відносять добре відомий детектор облич Віоли-Джонса. Разом з тим, існують певні фактори, які роблять процедуру розпізнавання обличчя унікальною. Серед них важливі такі:
Точність проти ефективності. Алгоритми розпізнавання загальних об'єктів призначені для пошуку грубого положення об'єкта на зображенні; детектори особливих точок обличчя потребують більш точної оцінки положення. Похибка в декілька пікселів вважається не вартою уваги при розпізнаванні об'єкта, але може вказувати на різницю між усмішкою і сумним виразом обличчя, які відслідковує детектор.
Невизначеність в результаті обмеженої просторової підтримки. Прийнято вважати, що зоні інтересу на зображенні відповідає така структура, яку легко виокремити від інших фрагментів, що не містять потрібного об'єкта. Для особливих точок обличчя не завжди виконується, у подібних випадках зазвичай маємо обмежену просторову підтримку. Фрагменти без потрібних особливих точок часто мають структуру, схожу на елементи обличчя. Наприклад елемент з боку обличчя, що знаходиться в середині відповідного обмежувального куба, можна легко переплутати з іншою частиною зображення, через центр якої проходить чітка межа.
Обчислювальна складність. Метою загального розпізнавання об'єктів є пошук усіх його екземплярів на зображенні. З іншого боку, алгоритму відслідковування обличчя необхідні позиції особливих точок обличчя, яких може бути від 20 до 100. Тому підвищення ефективності детектора є найважливішою задачею у побудові відслідковувача, що діє в режимі реального часу.
Через подібні відмінності детектори особливих точок обличчя, що використовуються для відслідковання, мають специфічні особливості і враховують мету застосунку. Звісно, багато технік загального розпізнавання об'єктів використовуються й для детекторів обличчя. Але в спільноті досі немає єдиної думки про те, який підхід краще вирішує проблему.
У цій частині детектори особливих точок обличчя будуватимемо на основі найпростішої моделі - лінійного фрагмента зображення. Не зважаючи на простоту, детальна розробка процедури навчання дозволить створити модель, здатну формувати прийнятні значення положення особливих точок для використання алгоритму відслідковування обличчя. Більше того, простота процедури забезпечує дуже швидкі обчислення, завдяки чому її можна виконати в режимі реального часу. Через представлення облич у вигляді фрагментів детектори відповідних особливих точок називають моделями фрагментів. Така модель реалізована у класі patch_model, що зберігається у файлах patch_model.hpp та patch_model.cpp. Далі показано код заголовоку класу patch_model, що описує його головну функціональність.
class patch_model{
public:
Mat P; // нормалізований фрагмент
...
Mat // карта результатів
calc_response(
const Mat &im, // фрагмент зображення в зоні пошуку
const bool sum2one = false); // нормалізуємо суму до одиниці?
...
void
train(const vector<Mat> &images, // тренувальні фрагменти
const Size psize, // розмір фрагмента
const float var = 1.0, // ідеальна дисперсія результатів
const float lambda = 1e-6, // вага стандартизації
const float mu_init = 1e-3, // початковий розмір кроку
const int nsamples = 1000, // кількість примірників
const bool visi = false); // процес візуалізації?
...
};
Модель фрагмента, що використовується для пошуку характеристик обличчя, зберігається у матриці P. Нас цікавлять дві функції класу: calc_responce та train. Функція calc_responce обчислює результат зміщення числових значень моделі фрагменту у регіоні пошуку im. Функція learn навчає модель фрагменту P розмірності psize, яка надає карти результатів для тренувальних наборів, розміщені якомога ближче до ідеальної карти. Параметри var, lambda, mu_init та nsamples - це параметри процедури, які можна налаштувати для оптимізації обробки даних. Функціональність класу пояснимо далі в цій частині.
Почнемо з обговорення кореляції фрагмента та його тренувальної процедури, яка використовується для навчання моделі. Далі опишемо класс patch_models, у якому зберігаються моделі для кожної характеристики обличчя та функції для глобальних перетворень. Програми train_patch_model.cpp та visualize_patch_mode.cpp відповідно тренують та візуалізують моделі фрагментів, а їх використання пояснимо детальніше в кінці розділу.
Засновані на кореляції моделі фрагментів
В детекторах, що навчаються, існують дві головні конкуруючі парадигми: генеративна та дискримінативна. Генеративні методи навчають представлення фрагментів зображень і здатні генерувати найкращий вигляд об'єкта у всіх його проявах. Дискримінативні, з іншого боку, формують представлення, що найефективніше відокремлюють його екземпляри від інших об'єктів, які можуть трапитися в процесі обробки. Генеративні методи кодують специфічні для об'єкта властивості, що надає змогу візуально перевірити наявні екземпляри. До популярних підходів, заснованих на цій парадигмі, відносять також відомий метод власних векторів Eigenfaces.
Дискримінативні методи використовують усі дані моделі для вирішення проблеми, дозволяючи відрізняти екземпляри одних об'єктів від інших. Найбільш відомим з них є метод опорних векторів. Хоча обидві парадигми діють добре в різних ситуаціях, якщо моделювати характеристики обличчя у вигляді фрагмента зображення, дискримінативна формують набагато кращі результати.
Примітка
Методи власних та опорних векторів розроблені для класифікації, а не розпізнавання чи вирівнювання зображень. Але їх математичні концепції можна використати й для відслідковування облич.
Навчаємо дискримінативні моделі фрагментів
Маючи анотований набір даних, можна навчити детектори характеристик незалежно один від одного. Метою навчання дискримінативної моделі є побудова такого фрагмента, який у результаті кроскореляції із фрагментом зображення, що містить характеристику обличчя, формує сильні відповіді в її позиції та слабкі в інших місцях. Математично це можна представити так:
У цьому випадку P відповідає моделі фрагмента, I позначає i-те тренувалльне зображення, I(a:b, c:d) - прямокутний блок з лівим верхнім та правим нижнім кутами, розміщеними у точках (a,c) та (b,d) відповідно. Символ періоду позначає операцію внутрішнього добутку, а R - ідеальну карту відповідей.
Модель фрагмента, що є рішенням цього рівняння, генерує карти відповідей, які в середньому наближаються до ідеальної на основі методу найменших квадратів. Очевидним вибором ідеальної карти R є матриця, усі елементи якої, крім середнього, дорівнюють нулю (якщо припустити, що в центрі фрагментів тренувальних зображень розміщені характеристики). На практиці, оскільки зображення анотовані вручну, завжди існуватимуть похибки. Щоб їх врахувати, краще описати R як функцію гниття, що залежить від відстані до центра. Непоганим вибором є двовимірний розподіл Гауса, у такому разі припускаємо, що похибки анотування відповідно розподілені. Візуалізація цих налаштувань показана далі, на зовнішньому куті лівого ока:
Як зазначено раніше, мета навчання вказана у формі, яку зазвичай називають методом найменших лінійних квадратів. Це дає змогу знайти рішення в закритій формі. Але варто зауважити, що ступінь свободи проблеми, тобто кількість варіантів змінних, які застосовуються для вирішення проблеми, дорівнює кількості пікселів відповідного фрагмента. Тому вартість обрахунків та вимоги до пам'яті для пошуку оптимальної моделі фрагмента можуть бути значними навіть для невеликих примірників. Наприклад, модель фрагмента з розмірами 40х40 має 1600 ступенів свободи.
Ефективною альтернативною вирішенню проблеми навчання є лінійна система рівнянь, яку ще називають методом стохастичного градієнтного спуску. Візуалізуючи мету навчання у вигляді похибки в залежності від ступенів свободи моделі фрагмента, цей процес поступово апроксимує градієнтний напрям та здійснює невеликий крок у протилежному до нього. У випадку нашої проблеми наближання градієнта можна вирахувати на основі одного, випадково обраного зображення з тренувального набору:
У класі patch_model процес навчання реалізований функцією train:
void
patch_model::train(
const vector<Mat> &images, // відцентровані тренувальні кадри
const Size psize, // бажаний розмір моделі фрагмента
const float var, // дисперсія похибок анотації
const float lambda, // параметр регуляризації
const float mu_init, // початкове значення кроку
const int nsamples, // кількість стохастичних примірників
const bool visi){ // візуалізація тренувального процесу
int N = images.size(),n = psize.width*psize.height;
int dx = wsize.width-psize.width; // центр карти відповідей
int dy = wsize.height-psize.height; //...
Mat F(dy,dx,CV_32F); // ідеальна карта відповідей
for(int y = 0; y < dy; y++){ float vy = (dy-1)/2 - y;
for(int x = 0; x < dx; x++){float vx = (dx-1)/2 - x;
F.fl(y,x) = exp(-0.5*(vx*vx+vy*vy)/var); // розподіл Гауса
}
}
normalize(F,F,0,1,NORM_MINMAX); // нормуємо в діапазон [0:1]
// виділяємо пам'ять
Mat I(wsize.height,wsize.width,CV_32F);
Mat dP(psize.height,psize.width,CV_32F);
Mat O = Mat::ones(psize.height,psize.width,CV_32F)/n;
P = Mat::zeros(psize.height,psize.width,CV_32F);
// оптимізуємо за допомогою стохастичного градієнтного спуску
RNG rn(getTickCount()); // генератор випадкового числа
double mu=mu_init,step=pow(1e-8/mu_init,1.0/nsamples);
for(int sample = 0; sample < nsamples; sample++){
int i = rn.uniform(0,N); // випадковий індекс зображення-примірника
I = this->convert_image(images[i]); dP = 0.0;
for(int y = 0; y < dy; y++){ // обчислюємо стохастичний градієнт
for(int x = 0; x < dx; x++){
Mat Wi=I(Rect(x,y,psize.width,psize.height)).clone();
Wi -= Wi.dot(O); normalize(Wi,Wi); // нормуємо
dP += (F.fl(y,x) – P.dot(Wi))*Wi;
}
}
P += mu*(dP - lambda*P); // здійснюємо невеликий крок
mu *= step; // зменшуємо розмір кроку
...
}return;
}
Перший виділений фрагмент коду у лістингу описує процес обчислення ідеальної карти відповідей. Оскільки зображення містять у центрі характеристики обличчя, карта не змінюється для всіх примірників. Другий фрагмент визначає такі значення швидкості гниття та кроку, для яких після nsamples ітерацій значення розміра кроку зменшиться до рівня, близького до нуля. Третій блок коду вказує на обчислення стохастичного градієнтного напрямку та оновлення моделі фрагмента. Тут маємо пояснити дві речі. По-перше, використані зображення передаються функції patch_model::convert_image, яка конвертує їх в одноканальні екземпляри (якщо вони кольорові) та застосовує натуральний логарифм до інтенсивності пікселів:
I += 1.0; log(I,I);
Значення зсуву, рівне одиниці, додається до кожного пікселя до виклику функції логарифма, оскільки для нуля він невизначений. Виконання попередньої обробки тренувальних зображень необхідне тому, що масштабовані більш стійкі до змін контрасту та умов освітлення. Наступний малюнок зображає два обличчя з різними ступенями контрасту. Різниця між зображеннями, масштабованими з використанням логарифма, менш помітна, ніж для необроблених примірників.
Рівняння оновлення має ще одну цікаву особливість - віднімання значення lambda*P від напряму. Ця процедура, яка часто застосовується в алгоритмах машинного навчання для забезпечення одноманітності нових даних, дозволяє обмежити рішення. Множник масштабування lambda визначений користувачем і залежить від проблеми. Але невеликі значення типово працюють краще для навчання моделей фрагментів, що використовуються для розпізнавання характеристик обличчя.
Генеративні та дискримінативні моделі фрагментів
Не зважаючи на простоту навчання дискримінативних моделей фрагментів, описаних раніше, спочатку варто перевірити, чи не легше отримати ті ж значення за допомогою генеративної парадигми та відповідного тренування. Генеративною противагою моделі кореляції виступає середній фрагмент. Метою навчання у такому разі є побудова єдиного фрагмента зображення, розміщеного максимально близько до всіх можливих примірників характеристик обличчя згідно критерію найменших квадратів:
Рішенням цієї проблеми є середнє усіх фрагментів тренувальних зображень, що містять характеристику в центрі. Вочевидь, подібні рішення значно легше знайти
Наступна ілюстрація порівнює карти відповідей, що отримані крос-кореляцією середнього та кореляційними моделями фрагмента на основі тренувального зображення. Також показані відповідні середнє та моделі фрагментів, діапазон значень пікселів на яких нормовано для цілей візуалізації. Хоча обидва типи моделей дещо схожі, генеровані ними карти відповідей суттєво відрізняються. Коли їх генерують кореляційні моделі фрагментів, відповіді займають пікові положення навколо характеристики. Карта відповідей, згенерована середньою моделлю, занадто гладка й не здатна чітко відрізнити положення особливої точки від тих, що розміщені поряд.
Дослідивши вигляд моделей фрагментів, здобудемо цікаві особливості. Кореляційна практично повністю сіра, що відповідає нулю в ненормованому діапазоні пікселів, з високими додатніми та від'ємними значеннями, що стратегічно розміщені навколо важливих зон характеристики обличчя. Тобто зберігаються лише компоненти тренувальних фрагментів, корисні для відмежування від невирівнених конфігурацій, що призводить до пікових значень відповідей. На противагу, середня модель не кодує відомості про невирівнені дані. У результаті вона не підходить для задачі локалізації характеристики обличчя, яка має відрізняти вирівнений фрагмент зображення від його локально зміщених варіантів.
Враховуємо глобальні геометричні перетворення
Досі ми припускали, що тренувальні зображення нормовані та відцентровані на характеристиці обличчя з врахуванням глобальних значень масштабування та повороту. На практиці в процесі відслідковування зображення обличчя ці параметри можуть набувати будь-яких значень. Тому механізм має врахувати відмінності між тренувальними та тестовими умовами. Один з підходів вимагає штучної зміни масштабів та повороту тренувальних примірників у межах, які вимагаються від готової програми. Але така спрощена форма детектора часто не дозволяє кореляційній моделі фрагмента сформувати корисні карти відповідей для подібних даних.
З іншого боку, кореляційна модель не дуже стійка до малих коливань масштабу та повороту. Оскільки рух між послідовними кадрами відеоряду порівняно невеликий, можна скористатися знайденими глобальними перетвореннями для попереднього кадра, щоб нормувати поточне зображення відповідно до масштабу та повороту. Все, що вимагається для процедури, - обрати кадр з навченими кореляційними моделями.
Клас patch_models зберігає кореляційні моделі фрагментів для кожної характеристики обличчя, а також кадр, використаний для тренування. Саме до класу patch_models, а не patch_model, напряму звертається код відслідковування для того, аби знайти характеристики. Наступний фрагмент коду - це оголошення класу та його основна функціональність:
class patch_models{
public:
Mat reference; // примірник форми [x1;y1;...;xn;yn]
vector<patch_model> patches; // модель фрагмента/характеристика
...
void
train(ft_data &data, // анотоване зображення та дані форми
const vector<Point2f> &ref, // форма примірника
const Size psize, // бажаний розмір фрагмента
const Size ssize, // розмір тренувального вікна пошуку
const bool mirror = false, // використовувати віддзеркалені дані
const float var = 1.0, // дисперсія похибок анотації
const float lambda = 1e-6, // вага регуляризації
const float mu_init = 1e-3, // початковий розмір кроку
const int nsamples = 1000, // кількість примірників
const bool visi = false); // візуалізація тренувальної процедури
...
vector<Point2f>// положення пікових значень характеристик
calc_peaks(
const Mat &im, // зображення для розпізнавання характеристик
const vector<Point2f> &points, // поточна оцінка форми
const Size ssize = Size(21,21)); // розмір вікна пошуку
...
};
Форма примірника зберігається у *interleaved* наборі координат (x,y), які використовуються для нормування значень масштабу та повороту тренувальних зображень та пізніше, у процесі обробки тестових зображень. Функція patch_models::train спершу знаходить перетворення подібності між примірником та анотованою формою заданого фрагмента за допомогою patch_models::calc_simil, яке шукає рішення тієї ж проблеми, що й shape_model::procructes, але лише для однієї пари вхідних даних. Оскільки поворот та масштабування не змінюється для усіх характеристик обличчя, процедура нормування зображення має лише відрегулювати перетворення подібності з урахуванням центрів кожної особливої точки на зображенні та центру нормованого фрагмента. У функції patch_models::train це реалізовано таким чином:
Mat S = this->calc_simil(pt),A(2,3,CV_32F);
A.fl(0,0) = S.fl(0,0); A.fl(0,1) = S.fl(0,1);
A.fl(1,0) = S.fl(1,0); A.fl(1,1) = S.fl(1,1);
A.fl(0,2) = pt.fl(2*i ) - (A.fl(0,0)*(wsize.width -1)/2 +
A.fl(0,1)*(wsize.height-1)/2);
A.fl(1,2) = pt.fl(2*i+1) – (A.fl(1,0)*(wsize.width -1)/2 +
A.fl(1,1)*(wsize.height-1)/2);
Mat I; warpAffine(im,I,A,wsize,INTER_LINEAR+WARP_INVERSE_MAP);
Змінна wsize - це загальний розмір нормованих тренувальних зображень, тобто сума розмірів фрагмента та зони пошуку. Раніше ми вже зазначали, що ліва верхня частина (2х2) перетворень подібності між формами примірника та анотованою pt, яка позначає компоненти масштабу та повороту, зберігається в афінній матриці, яка передається функції warpAffine бібліотеки OpenCV. Остання колонка афінних перетворень A - це налаштування, яке дозволить відобразити положення i-тої характеристики обличчя в центрі нормованого зображення після викривлення (тобто нормоване переміщення).
Нарешті, функція cv::warpAffine має налаштування викривлення між зображенням та примірником за замовчуванням. Оскільки перетворення подібності обраховані для перетворення форми примірника у простір анотацій pt, маємо встановити відмітку WARP_INVERSE_MAP для того, щоб функція виконала викривлення у бажаному напрямі. Така ж процедура здійснюється у функції patch_models::calc_peaks, але у цьому разі існує й додатковий крок - знайдене перетворення подібності між примірником та поточною формою на кадрі використовується для рознормування знайдених характеристик обличчя з метою їх коректного розміщення на зображенні:
vector<Point2f>
patch_models::calc_peaks(const Mat &im,
const vector<Point2f> &points,const Size ssize){
int n = points.size(); assert(n == int(patches.size()));
Mat pt = Mat(points).reshape(1,2*n);
Mat S = this->calc_simil(pt);
Mat Si = this->inv_simil(S);
vector<Point2f> pts = this->apply_simil(Si,points);
for(int i = 0; i < n; i++){
Size wsize = ssize + patches[i].patch_size();
Mat A(2,3,CV_32F),I;
A.fl(0,0) = S.fl(0,0); A.fl(0,1) = S.fl(0,1);
A.fl(1,0) = S.fl(1,0); A.fl(1,1) = S.fl(1,1);
A.fl(0,2) = pt.fl(2*i ) - (A.fl(0,0)*(wsize.width -1)/2 +
A.fl(0,1)*(wsize.height-1)/2);
A.fl(1,2) = pt.fl(2*i+1) – (A.fl(1,0)*(wsize.width -1)/2 +
A.fl(1,1)*(wsize.height-1)/2);
warpAffine(im,I,A,wsize,INTER_LINEAR+WARP_INVERSE_MAP);
Mat R = patches[i].calc_response(I,false);
Point maxLoc; minMaxLoc(R,0,0,0,&maxLoc);
pts[i] = Point2f(pts[i].x + maxLoc.x - 0.5*ssize.width,
pts[i].y + maxLoc.y - 0.5*ssize.height);
}return this->apply_simil(S,pts);
У першій виділеній частині коду обчислюються прямі та зворотні перетворення подібності. Зворотні потрібні для того, аби мати можливість відрегулювати пікові значення карти відповідей для кожної характеристики, врахувавши нормовані положення поточної форми. Це треба зробити до повторного застосування перетворень подібності для переміщення нових положень характеристик на зображення за допомогою функції patch_models::apply_simil.
Тренування та візуалізація
Приклад програми для тренування моделей фрагмента на основі анотованих даних знаходиться у файлі train_patch_model.cpp. Якщо в аргументі командної стрічки argv[1] вкажемо шлях до анотованих даних, тренування почнеться із завантаження даних у пам'ять та видалення неповних примірників:
ft_data data = load_ft<ft_data>(argv[1]);
data.rm_incomplete_samples();
Найпростішим варіантом форми примірника для класі patch_models є середня, масштабована до бажаних розмірів. Припустимо, що модель заздалегідь натреновано для цього набору даних, у такому разі на першому кроці обчислення форми примірника завантажимо дані, що зберігаються в аргументі argv[2]:
shape_model smodel = load_ft<shape_model>(argv[2]);
Далі відбувається процес обчислення масштабованої відцентрованої середньої форми:
smodel.p = Scalar::all(0.0);
smodel.p.fl(0) = calc_scale(smodel.V.col(0),width);
vector<Point2f> r = smodel.calc_shape();
Функція calc_scale знаходить множник масштабування для перетворення середньої форми (тобто першої колонки shape_model::V) у квадратну з розмірами width x width. Як тільки визначимо форму примірника r, тренування набору моделей фрагментів можна здійснити єдиним викликом функції:
patch_models pmodel;
pmodel.trail(data,r,Size(psize,psize),Size(ssize,ssize));
Оптимальний вибір параметрів width, psize та ssize залежить від застосунку; але відповідні значення за замовчуванням (100, 11 та 11) у загальних випадках дають прийнятні результати.
Хоча процес тренування доволі простий, йому може знадобитися певний час. Залежно від кількості характеристик обличчя, розміру фрагментів та кількості стохастичних примірників в алгоритмі оптимізації, процес тренування може зайняти від декількох хвилин до понад години. Але оскільки тренування кожного фрагмента можна здійснити незалежно від інших, процес можна значно прискорити за рахунок багатопотокового рішення, що виконується на декількох процесорних ядрах чи навіть машинах.
Коли завершиться тренування, застосунок visualize_patch_model.cpp можна використати для візуалізації отриманих моделей форм. Як і в випадку застосунку visualize_shape_model.cpp, ціллю у цьому разі є візуальна перевірка результатів на коректність. Програма генерує складене зображення усіх моделей форм, patch_model::P, кожне розміщене в центрі відповідного положення характеристики на формі примірника, patch_models::reference, та зображає обмежувальний прямокутник фрагмента з активним індексом.
Функція cv::waitKey потрібна для вибору індекса активного фрагмента та завершення роботи програми. Наступний малюнок ілюструє три приклади складених зображень, отриманих шляхом навчання моделі фрагментів з різною просторовою підтримкою. Не зважаючи на використання тих же тренувальних даних, зміна просторової підтримки моделі фрагмента суттєво впливає на її структуру. Візуальна перевірка результатів допомагає регулювати параметри тренувального процесу, щоб оптимізувати процедуру для конкретного застосунку.
Розпізнавання обличчя та налаштування процесу
Вищезазначений метод відслідковування обличчя припускає, що характеристики розміщені достатньо близько до поточної оцінки. Хоча цей вибір прийнятний для відслідковування, адже рух обличчя між кадрами досить незначний, ми все ще маємо коректно налаштувати модель для першого зображення послідовності. Очевидним вибором є застосування вбудованого в OpenCV каскадного детектора для розпізнавання обличчя. Однак, розміщення моделі у межах визначеного обмежувальної коробки залежатиме від вибору характеристики для відслідковування. Згідно з парадигмою даних, якої ми дотримувались у даному розділі, простим рішенням є вивчення геометричної залежності між обмежувальною формою розпізнавання та характеристиками обличчя.
Клас face_detector реалізує саме це рішення. Виділений фрагмент оголошення має такий вигляд:
class face_detector{ // детектор обличчя для налаштування
public:
string detector_fname; // файл з каскадним класифікатором
Vec3f detector_offset; // зміщення від центра розпізнавання
Mat reference; // форма примірника
CascadeClassifier detector; // детектор обличчя
vector<Point2f> // точки, що описують знайдене обличчя
detect(const Mat &im, // зображення обличчя
const float scaleFactor = 1.1,// збільшення масштабу
const int minNeighbours = 2, // мінімальна кількість сусідів
const Size minSize = Size(30,30));// мінімальний розмір вікна
void
train(ft_data &data, // тренувальні дані
const string fname, // каскадний детектор
const Mat &ref, // форма примірника
const bool mirror = false, // дзеркальні дані?
const bool visi = false, // візуалізаіція тренування?
const float frac = 0.8, // відсоток розпізнаних точок
const float scaleFactor = 1.1, // множник масштабу
const int minNeighbours = 2, // мінімальна кількість сусідів
const Size minSize = Size(30,30)); // мінімальний розмір вікна
...
};
Клас має чотири публічні змінні: шлях до об'єкта типу cv::CascadeClassifier з назвою detector_fname, зміщення від обмежувальної коробки розпізнавання до дійсного положення обличчя detector_offset, форма примірника для розміщення та власне детектор обличчя. Головною функцією системи відслідковування є face_detector::detect, вхідними параметрами якого є зображення та налаштування класу cv::CascadeClassifier, а результатом - приблизна оцінка положення характеристик. Його реалізовано наступним чином:
Mat gray; // конвертуємо зображення у відтінки сірого та вирівнюємо гістограму
if(im.channels() == 1)gray = im;
else cvtColor(im,gray,CV_RGB2GRAY);
Mat eqIm; equalizeHist(gray,eqIm);
vector<Rect> faces; // визначаємо найбільше обличчя на зображенні
detector.detectMultiScale(eqIm,faces,scaleFactor,
minNeighbours,0
|CV_HAAR_FIND_BIGGEST_OBJECT
|CV_HAAR_SCALE_IMAGE,minSize);
if(faces.size() < 1){return vector<Point2f>();}
Rect R = faces[0]; Vec3f scale = detector_offset*R.width;
int n = reference.rows/2; vector<Point2f> p(n);
for(int i = 0; i < n; i++){ // прогнозуємо розміщення обличчя
p[i].x = scale[2]*reference.fl(2*i ) +
R.x + 0.5 * R.width + scale[0];
p[i].y = scale[2]*reference.fl(2*i+1) +
R.y + 0.5 * R.height + scale[1];
}return p;
Обличчя розпізнаємо як і раніше, за винятком використаної відмітки CV_HAAR_FIND_BIGGEST_OBJECT, потрібної для пошуку найбільшого обличчя серед вхідних даних. Виділений код вказує на розміщення форми примірника відповідно до знайденої обмежувальної коробки обличчя. Змінна detector_offset складається з трьох компонентів: зміщення (x,y) центру обличчя від центру форми розпізнавання, а також множник масштабування, який змінює розміри форми примірника, щоб найкраще розмістити обличчя на зображенні. Усі три компоненти лінійно залежні від ширини обмежувальної коробки.
Лінійна залежність між шириною обмежувальної коробки та змінною detector_offset отримується шляхом навчання анотованих даних у функції face_detector::train. Процес навчання починається із завантаження зображень у пам'ять та призначення форми примірника:
detector.load(fname.c_str()); detector_fname = fname; reference = ref.clone();
Як і раніше в класі patch_models, формою примірника краще обрати нормоване середнє обличчя набору даних. Потім до кожного зображення (а також до його дзеркального відображення за необхідності) застосовується каскадний класифікатор cv::CascadeClassifier, а результат розпізнавання перевіряється, щоб достатня кількість анотованих точок лежала в межах знайденої обмежувальної коробки (дивіться малюнок в кінці частини), аби процедура навчання не здіснювала хибні розпізнавання:
if(this->enough_bounded_points(pt,faces[0],frac)){
Point2f center = this->center_of_mass(pt);
float w = faces[0].width;
xoffset.push_back((center.x -
(faces[0].x+0.5*faces[0].width ))/w);
yoffset.push_back((center.y -
(faces[0].y+0.5*faces[0].height))/w);
zoffset.push_back(this->calc_scale(pt)/w);
}
Якщо більше ніж frac відсотків анотованих точок лежить в обмежувальній коробці, лінійна залежність між його шириною та параметрами зміщення додається як новий запис в об'єкт вектора STL. Функція face_detector::center_of_mass визначає центр мас набору анотованих точок для зображення, а функція face_detector::calc_scale - обчислює множник масштабування для перетворення примірника у відцентровану анотовану форму. Коли будуть оброблені всі зображення, змінна detector_offset вказуватиме на медіану всіх окремих зміщень.
Mat X = Mat(xoffset),Xsort,Y = Mat(yoffset),Ysort,Z =
Mat(zoffset),Zsort;
cv::sort(X,Xsort,CV_SORT_EVERY_COLUMN|CV_SORT_ASCENDING);
int nx = Xsort.rows;
cv::sort(Y,Ysort,CV_SORT_EVERY_COLUMN|CV_SORT_ASCENDING);
int ny = Ysort.rows;
cv::sort(Z,Zsort,CV_SORT_EVERY_COLUMN|CV_SORT_ASCENDING);
int nz = Zsort.rows;
detector_offset =
Vec3f(Xsort.fl(nx/2),Ysort.fl(ny/2),Zsort.fl(nz/2));
Як і для моделей форми та фрагмента, проста програма train_face_detector.cpp пояснює приклад побудови та збереження об'єкта face_detector, щоб існувала можливість його подальшого використання. Спочатку вона завантажує анотовані дані та модель форми, а також присвоює формі примірника відцентроване середнє тренувальних даних (тобто форму ідентичності у класі shape_model):
ft_data data = load_ft<ft_data>(argv[2]);
shape_model smodel = load_ft<shape_model>(argv[3]);
smodel.set_identity_params();
vector<Point2f> r = smodel.calc_shape();
Mat ref = Mat(r).reshape(1,2*r.size());
Тренування та зберігання детектора обличчя у такому разі складається з викликів двох функцій:
face_detector detector;
detector.train(data,argv[1],ref,mirror,true,frac);
save_ft<face_detector>(argv[4],detector);
Щоб протестувати продуктивність отриманої процедури розміщення форм, застосунок visualize_face_detector.cpp викликає функцію face_detector::detect для кожного зображення вхідного потоку відео чи камери та зображає отримані результати на екрані. Приклад цього підходу показаний на наступному малюнку. Хоча розміщена форма не співпадає з окремою на зображенні, її позиція досить близька, аби відслідковувач обличчя отримав результати за допомогою описаного далі підходу:
Відсліковування обличчя
Задачею відслідковування обличчя є пошук ефективного та надійного способу об'єднати незалежні розпізнавання різних характеристик обличчя разом з їх геометричними залежностями для того, щоб отримати точну оцінку позицій на кожному зображенні послідовності. Враховуючи це, варто подумати на тим, чи потрібні взагалі геометричні залежності. Далі показані результати розпізнавання характеристик обличчя з відповідними обмеженнями та без них.
Ці результати досить чітко вказують на користь застосування просторових залежностей між характеристиками обличчя. Відносна продуктивність обох підходів типова, але покладатися винятково на розпізнавання не можна, це призводить до занадто зашумлених рішень. Карти відповідей для кожної характеристики не завжди мають пікові значення в коректних позиціях. Через зашумленість зображення, зміни умов освітлення чи виразу обличчя єдиним способом перебороти обмеження детекторів характеристик є застосування геометричних відношень між собою.
Особливо простий і водночас дивовижно ефективний спосіб доєднати геометрію обличчя до процедури відслідковування є проєкція результатів розпізнавання характеристик на лінійний підпростір моделі форм. Це включає мінімізацію відстані між початковими точками та найближчою прийнятною формою, що лежить у підпросторі. Таким чином, якщо просторовий шум розпізнавання має розподіл, близький до Гауса, проєкція надасть найвірогідніше рішення. На практиці розподіл похибок розпізнавання не має такого вигляду, тому потрібні додаткові механізми.
Реалізація відслідковування обличчя
Реалізація алгоритму відслідковування обличчя знаходиться у класі face_tracker (файли face_tracker.cpp та face_tracker.hpp). Далі покажемо фрагмент коду, у якому пояснюється його головна функціональність:
class face_tracker{
public:
bool tracking; // чи знаходимося ми в режимі відслідковування
fps_timer timer; // кількість кадрів/секундний таймер
vector<Point2f> points; // поточні знайдені точки
face_detector detector; // детектор для налаштування
shape_model smodel; // модель форми
patch_models pmodel; // детектори характеристик
face_tracker(){tracking = false;}
int // 0 = помилка
track(const Mat &im, // зображення з обличчям
const face_tracker_params &p = // параметри для відслідковування
face_tracker_params()); // параметри за замовчуванням
void
reset(){ // перезавантаження відслідковувача
tracking = false; timer.reset();
}
...
protected:
...
vector<Point2f> // знайдені точки на зображенні
fit(const Mat &image, // зображення з обличчям
const vector<Point2f> &init, // початкові оцінки точок
const Size ssize = Size(21,21),// розмір зони пошуку
const bool robust = false, //використовувати точне розміщення?
const int itol = 10, // максимальна кількість ітерацій
const float ftol = 1e-3); // толерантність до збіжності
};
Клас має публічні об'єкти класів shape_model, patch_models та face_detector і використовує їх функціональність для ефективного відслідковування. Таймер - це екземпляр класу fps_timer, який відслідковує швидкість виконання функції face_tracker::track в кадрах в секунду. Він корисний для аналізу впливу фрагмента та конфігурації моделі форми на обчислювальну складність алгоритму.
Змінна tracking - це відмітка, яка взаує на поточний стан процедури відслідковування. Якщо її встановлено в значення false, як у конструкторі та функції face_tracker::reset, об'єкт переходить у режим розпізнавання, за якого функція face_detector::detect застосовується до наступного вхідного зображення для налаштування моделі. У режимі відслідковування початкова оцінка, що використовується для позначень позицій характеристик обличчя, - це просто їх положення на попередньому кадрі. Повний алгоритм відслідковування реалізовано таким чином:
int face_tracker::
track(const Mat &im,const face_tracker_params &p){
Mat gray; // конвертуємо зображення у відтінки сірого
if(im.channels()==1)gray=im;
else cvtColor(im,gray,CV_RGB2GRAY);
if(!tracking) // налаштовуємо
points = detector.detect(gray,p.scaleFactor,
p.minNeighbours,p.minSize);
if((int)points.size() != smodel.npts())return 0;
for(int level = 0; level < int(p.ssize.size()); level++)
points = this->fit(gray,points,p.ssize[level],
p.robust,p.itol,p.ftol);
tracking = true; timer.increment(); return 1;
}
Крім загальних операцій, як-от вибору коректного стану та інкрементації часу відслідковування, алгоритм є багаторівненою процедурою вміщення, це виділено на попередньому фрагменті коду. Алгоритм, реалізований у функції face_tracker::fit, застосовується декілька разів з різними розмірами вікон пошуку, шляхом виклику face_tracker_params::ssize, причому результат попередьного етапу використовується як вхідні дані для наступного. У найпростішому варіанті функція face_tracker_params::ssize виконує розпізнавання характеристик обличчя навколо поточної оцінки форми на зображенні:
smodel.calc_params(init);
vector<Point2f> pts = smodel.calc_shape();
vector<Point2f> peaks = pmodel.calc_peaks(image,pts,ssize);
Код також проєктує результат на підпростір форми обличчя:
smodel.calc_params(peaks);
pts = smodel.calc_shape();
Щоб врахувати зайві елементи серед знайдених позицій характеристик обличчя, замість простої проєкцій можна скористатися процедурою порівняння з точною моделлю, встановивши відповідне значення у положення true. Але на практиці, зі змінним розміром вікна пошуку (встановленим у face_tracker_params::ssize), це часто не має значення, адже зайві елементи типово розміщені далеко від відповідних точок проєктованої форми і, вочевидь, лежатимуть за межами зони пошуку наступного рівня процедури порівняння. Тобто зі зменшенням розмірів вікна пошуку зайві елементи поступово видаляються.
Тренування та візуалізація
На відміну від інших класів, що описані в цьому розділі, тренування об'єкта face_tracker не включає процесу навчання. Це реалізується в файлі train_face_tracker.cpp таким чином:
face_tracker tracker;
tracker.smodel = load_ft<shape_model>(argv[1]);
tracker.pmodel = load_ft<patch_models>(argv[2]);
tracker.detector = load_ft<face_detector>(argv[3]);
save_ft<face_tracker>(argv[4],tracker);
Аргументи від argv[1] до argv[4] містять шляхи до об'єктів shape_model, patch_model, face_detector та face_tracker відповідно. Візуалізація відслідковувача обличчя у файлі visualize_face_tracker.cpp доволі проста. Отримавши вхідне значення зображення з камери чи відеофайлу за допомогою класу cv::VideoCapture, програма просто здійснює процедуру відслідковування в циклі до кінця потоку або моменту, коли користувач натисне клавішу Q. Також існує можливість перезавантажити відслідковувач у будь-який час, натиснувши клавішу D.
Загальні та специфічні моделі
Деякі параметри процедур тренування та відслідковування можна налаштувати для оптимізації продуктивності певного застосунку. Разом з тим, одним з головних визначників якості відслідковування є діапазон форм та варіативність виглядів, які відслідковувач має змоделювати. Для конкретності розглянемо загальні моделі та визначені для певної особи. Загальну модель тренують анотованими зображеннями багатьох осіб, виразів, знятих за різних умов освітлення та інших параметрів. З іншого боку, специфічні моделі формуються для однієї особи. Тому потрібно врахувати набато меншу варіативність даних. У результаті алгоритм відслідковування однієї особи більш точний за загальний.
Це проілюстровано на наступному малюнку. Загальна модель натренована на основі набору даних MUCT. Специфічна модель натренована даними, що згенеровані з використанням описаного раніше інструменту для анотування зображень. Ілюстрація чітко показує кращі результати відслідковування для такої моделі, адже вона може знаходити складні вирази обличчя та зміни пози голови, у той час як загальні моделі ледве справляються з деякими простими їх варіантами
Варто зазначити, що розглянутий метод - це базовий підхід, який лише пояснює різні компоненти, що використовуються в більшості алгоритмів нецілісного відслідковування обличчя. Численні способи покращити різні елементи методу лежать за межами огляду цієї книги і потребують особливих математичних інструментів, які все ще не підтримуються бібліотекою OpenCV. Невелика кількість доступних програмних пакетів відслідковування обличчя комерційного рівня - це показник складності проблеми. Тим не менше, простий підхід, описаний в цьому розділі, може діяти достатньо добре в обмежених умовах.
Висновки
У цьому розділі ми побудували простий відслідковувач обличчя, який може працювати добре в обмежених умовах з використанням скромного математичного апарату та базової функціональності бібліотеки OpenCV для простої обробки зображень та лінійних алгебраїчних операцій. Покращити цей простий відслідковувач можна за рахунок використання більш складних технік для кожного з трьох компонентів: моделі форми, детекторів характеристик та алгоритму порівняння. Модульний дизайн відслідковувача, описаний у частині, має дозволити змінювати їх без суттєвого порушення роботи інших компонентів.
Посилання
Procrustes Problems, Gower, John C. and Dijksterhuis, Garmt B, Oxford University Press, 2004.
Внимание! Этот перевод, возможно, ещё не готов.
Его статус: перевод редактируется
Переведено на Нотабеноиде
http://notabenoid.org/book/81521/500885
Переводчики: Atronax
Настройки
Готово:
