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

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

У цьому розділі з'ясуємо, що відноситься до структури з руху (Structure for Motion, SfM), інакше кажучи, як отримувати геометричні структури із зображень за рахунок обробки кадрів з різних положень камери з використанням API OpenCV. Обмежимось однією (такий підхід частіше за все називають монокулярним), а також дискретним прорідженим набором кадрів замість послідовного потоку. Ці обмеження суттєво спростять систему, яку будуватимемо надалі, та допожуть зрозуміти основи методу SfM. Щоб реалізувати його, повторимо шлях Хартлі та Зіссермана (відтепер позначатимемо їх як Х. та З.), задокументований ними у розділах 9-12 книги "Multiple View Geometry in Computer Vision"

Надалі опишемо такі теми:
Концепції структури з руху
Оцінка руху камери на основі пари зображень
Відновлення сцени з одної позиції
Реконструкція з багатьох точок зору
Уточнення реконструкції
Візуалізація хмари 3D точок

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

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

Об'єднаємо також терміни для координати точки на сцені, світові, дійсні чи 3D як єдині, що існують в дійсності. Те ж відноситься до двовимірних точок на зображенні, які позначають елементи у площині координат зображення, що відповідають 3D-точкам, спроєктованим сенсором камери у певному місці й у певний час. У коді цього розділу ви побачите посилання на книгу "Multiple View Geometry in Computer Vision", наприклад // HZ 9.12. Це відноситься до рівняння 12 з дев'ятого розділу. Текст включатиме лише фрагменти коду, а повністю робоча програма доступна в матеріалах, що супроводжують книгу.

Концепції структури з руху

Спершу ми маємо зрозуміти відмінність між стерео (чи будь-яким іншим мультивидом), 3D-реконструкцією з використанням каліброваних наборів та структурою з руху (SfM). Маючи набір з двох або більше камер, ми вже припускаємо наявність руху між ними, у випадку ж SfM ця характеристика невідома, ми лише бажаємо визначити її. Калібровані системи, якщо сказати простіше, дозволяють зробити більш точну реконструкцію тривимірної геометрії, адже не існує похибки оцінки відстані та поворотів між камерами, ці характеристики вже відомі. Перший крок реалізації системи SfM - це визначення руху між камерами. Бібліотека OpenCV надає декілька способів отримати цей рух, наприклад за допомогою функції findFundamentalMat.

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

Насправді, пошук точки перетину променів - надто ненадійний метод; Х. та З. рекомендують не використовувати його. Причиною є те, що промені зазвичай не перетинаються, змушуючи йти на поступки та обирати середню точку на найкоротшому сегменті, що з'єднує два промені. Замість цього, Х. та З. пропонують декілька способів триангулювати точки, деякі з яких опишемо в частині "Реконструкція сцени". Поточна версія OpenCV не має простого API для триангуляції, тому цю частину запрограмуємо власноруч.

Відновивши тривимірну геометрію з двох точок зору, спробуємо під'єднати більше камер, напрямлених на ту ж сцену, щоб покращити якість реконструкції. На даному етапі більшість алгоритмів SfM намагатимуться оптимізувати набір знайдених позицій камер та тривимірних точок методом регулювання груп, що описаний в частині "Уточнення реконструкції". Бібліотека OpenCV зберігає відповідні функції в новому наборі для зшивання зображень. Разом з тим, краса роботи з С++ полягає в можливості легко інтегрувати та застосувати зовнішні інструменти. Тому спробуємо використати зовнішній регулятор груп, бібліотеку SSBA.

Поверхнево сформулювавши основи підходу до SfM з використанням OpenCV, перейдемо до реалізації кожного окремого елемента.

Визначаємо рух камери для двох зображень

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

Двома дуже важливими об'єктами є фундаментальна матриця F та базова E. Вони практично подібні, якщо не враховувати, що базова припускає використання каліброваних камер; нам потрібен саме цей варіант, тому обираємо його. Бібліотека OpenCV дозволяє знаходити лише фундаментальну матрицю за допомогою функції findFundamentalMat; однак, з неї дуже легко отримати базову, якщо застосувати матрицю калібрування K таким чином:
Mat_<double> E = K.t() * F * K; // згідно з Х. та З. (9.12)

Базова матриця розмірності 3х3 встановлює обмеження між двома зображеннями згідно формули x'Ex=0, де x - це точка на першому, а x' - відповідна їй на другому кадрі. Такий підхід дуже корисний, як виявиться дещо пізніше. Крім цього, використаємо ще один важливий факт: нам достатньо лише базової матриці для того, щоб відновити обидві камери із зображень, хоч і залежні від зміни розмірів. Але про це поговоримо пізніше. Отримавши базову матрицю, ми дізнаємось, де розміщені камери та як вони напрямлені. Можна легко вирахувати матрицю на основі певної кількості рівнянь обмежень, адже вирішення кожного з них формує в результаті невелику частину матриці. Насправді, OpenCV дозволяє обчислити її на основі 7 пар точок, але чим їх більше, тим точніший результат.

Порівнюємо точки на співпадіння з використанням розширених дескрипторів

Використаємо рівняння обмежень для обрахунку базової матриці. Щоб знайти їх, для кожної точки зображення A маємо знайти відповідну точку на кадрі B. Як отримати такі співпадіння? Просто використавши розширений фреймворк OpenCV для порівняння особливих точок, який набув значного розвитку за декілька останніх років.

Пошук особливих точок та перевірка дескрипторів на співпадіння - це базова процедура комп'ютерного зору. ЇЇ можна застосувати багатьма способами для реалізації будь-яких операцій, наприклад, розпізнавання позиції та орієнтації об'єкта або пошуку у великій базі даних зображень, подібних до запитуваного. Взагалі кажучи, процес виокремлення означає підбір достатньо якісних особливих точок та обрахунок відповідних дескрипторів. Це вектор значень, що описують середовище навколо обраного елемента. Різні методи формують неоднакові розміри та тип даних для дескрипторів. Перевірка на співпадіння - це процес порівняння особливих точок у двох наборах з використанням цих векторів. OpenCV пропонує дуже легкі та потужні методи для підтримки виокремлення та перевірки на співпадіння. Більше інформації щодо даної теми знаходиться в розділі 3 "Безмаркерна доповнена реальність".

Розглянемо дуже простий спосіб пошуку та порівняння особливих точок:
// розпізнаємо ключові точки
SurfFeatureDetector detector();
vector<KeyPoint> keypoints1, keypoints2;
detector.detect(img1, keypoints1);
detector.detect(img2, keypoints2);
// обчислюємо дескриптори
SurfDescriptorExtractor extractor;
Mat descriptors1, descriptors2;
extractor.compute(img1, keypoints1, descriptors1);
extractor.compute(img2, keypoints2, descriptors2);
// порівнюємо дескриптори на співпадіння
BruteForceMatcher<L2<float>> matcher;
vector<DMatch> matches;
matcher.match(descriptors1, descriptors2, matches);

Можливо, ви вже бачили подібний код OpenCV раніше, але переглянемо його ще раз. Маємо отримати три елементи: особливі точки для двох зображень, відповідні їм дескриптори та список співпадінь між наборами. Бібліотека OpenCV пропонує різноманітні детектори ключових точок, видобувачі дескрипторів та алгоритми порівняння. У цьому простому прикладі використаємо функцію SurfFeatureDetector, щоб отримати двовимірне положення SURF-точок, та SurfDescriptorExtractor, щоб знайти дескриптори SURF. Для перевірки використаємо алгоритм повного перебору, який вважається найпростішим способом знайти співпадіння шляхом порівняння кожної особливої точки першого набору з кожною другого (звідси й назва) та знайти накраще співпадіння.

Далі зображено співпадіння ключових точок на звох зображеннях послідовності Fountain-P11, що зберігається за адресою http://cvlab.epfl.ch/~strecha/multiview/denseMVS.html.

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

Перевіряємо точки на співпадіння на основі оптичного руху

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

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

Окрім перевірки фрагментів на співпадіння, нові методи оптичного руху застосовують інші додаткові способи, щоб досягнути кращих результатів. До них відносять й використання пірамід зображень, які є все меншими версіями початкового, що дозволяє працювати за методикою "від грубого до тонкого", - поширеною в комп'ютерному зорі. Інший метод потребує визначення глобальних обмежень для поля потоку з урахуванням припущення, що близькі точки "рухаються разом" у тому ж напрямі. Поглиблений огляд оптичного потоку можна знайти в розділі "Developing Fluid Wall Using the Microsoft Kinect", доступного на вебсайті Packt.

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

Vector<KeyPoint>left_keypoints,right_keypoints;
// Розпізнаємо ключові точки на лівому та правому зображеннях
FastFeatureDetector ffd;
ffd.detect(img1, left_keypoints);
ffd.detect(img2, right_keypoints);
vector<Point2f> left_points;
KeyPointsToPoints(left_keypoints,left_points);
vector<Point2f>right_points(left_points.size());
// Перетворюємо зображення у відтінки сірого
Mat prevgray,gray;
if (img1.channels() == 3) {
cvtColor(img1,prevgray,CV_RGB2GRAY);
cvtColor(img2,gray,CV_RGB2GRAY);
} else {
prevgray = img1;
gray = img2;
}

// Обчислюємо поле оптичного потоку:
// наскільки змінилося положення left_point за два зображення
vector<uchar>vstatus; vector<float>verror;
calcOpticalFlowPyrLK(prevgray, gray, left_points, right_points,
vstatus, verror);
// По-перше, відфільтровуємо точки з великою похибкою
vector<Point2f>right_points_to_find;
vector<int>right_points_to_find_back_index;
for (unsigned inti=0; i<vstatus.size(); i++) {
if (vstatus[i] &&verror[i] < 12.0) {
// Зберігаємо початковий індекс точки в
// масиві оптичного потоку, для майбутнього використання
right_points_to_find_back_index.push_back(i);
// Зберігаємо власне особливу точку
right_points_to_find.push_back(j_pts[i]);
} else {

} else {
vstatus[i] = 0; // неякісний потік
}
}
// для кожної right_point шукаємо, до якої знайденої точки він відноситься
Mat right_points_to_find_flat =
Mat(right_points_to_find).reshape(1,to_find.size()); // вирівнюємо масив
vector<Point2f>right_features; // знайдені особливі точки
KeyPointsToPoints(right_keypoints,right_features);
Mat right_features_flat =
Mat(right_features).reshape(1,right_features.size());
// Оглядаємо середовище навколо кожної точки OF
// на правому зображенні на наявність особливих точок
// та здійснюємо перевірку на співпадіння.

BFMatchermatcher(CV_L2);
vector<vector<DMatch>>nearest_neighbors;
matcher.radiusMatch(
right_points_to_find_flat,
right_features_flat,
nearest_neighbors,
2.0f);
// Перевіряємо унікальність знайдених сусідів (відкидаємо ті з них,
// що розміщені надто близько, адже вони можуть збити з пантелику)
std::set<int>found_in_right_points; // щоб запобігти дублюванню
for(inti=0;i<nearest_neighbors.size();i++) {
DMatch _m;
if(nearest_neighbors[i].size()==1) {
_m = nearest_neighbors[i][0]; // лише один сусід
} else if(nearest_neighbors[i].size()>1) {

// перевіряємо, наскільки близько розміщена пара точок
double ratio = nearest_neighbors[i][0].distance /
nearest_neighbors[i][1].distance;
if(ratio < 0.7) { // не надто близько
// обираємо найближчу (першу)
_m = nearest_neighbors[i][0];
} else { // занадто близько - не можна сказати, яка підходить краще
continue; // не проходить тест відношень – відкидаємо
}
} else {
continue; // сусідніх точок немає... :(
}
// запобігаємо дублюванню
if (found_in_right_points.find(_m.trainIdx) ==
found_in_right_points.end()) {
// Знайдену сусідню точку ще не використано
// Маємо перевірити її на співпадіння з
// початковим індексом лівої точки
_m.queryIdx = right_points_to_find_back_index[_m.queryIdx];
matches->push_back(_m); // додаємо це співпадіння
found_in_right_points.insert(_m.trainIdx);
}
}
cout<<"скорочено"<< matches->size() <<" / "<<nearest_neighbors.size()
<<" matches"<<endl;

Функції KeyPointsToPoints та PointsToKeyPoints - це просто зручні інструменти для перетворень між структурами даних cv::Point2f та cv::KeyPoint.
У попередньому блоці коду відбувається декілька цікавих речей. По-перше, коли ми працюємо з оптичним рухом, результат вказує на переміщення особливої точки з позиції на лівому зображенні у відповідну позицію на правому. Водночас, ми маємо новий набір ключових точок, знайдених на кадрі справа, не обов'язково відповідних до точок, що рухаються в оптичному потоці. Тому потрібно відновити ці відповідності. Щоб знайти загублені особливі точки, застосуємо пошук k найближчих сусідів (kNN), у результаті чого здобудемо до двох фрагментів, що належать зоні інтересу з радіусом 2 пікселів.

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

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

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

Шукаємо матриці камери

Маючи співпадіння між ключовими точками, можемо вирахувати фундаментальну та відповідну їй базову матриці. Але в першу чергу мусимо вирівняти масиви співпадінь таким чином, щоб індекс у першому відповідав тому ж індексу в другому. Цього вимагає функція findFundamentalMat. Також треба конвертувати структуру KeyPoint в Point2f. Особливу увагу приділимо внутрішнім змінним класу DMatch, queryIdx та trainIdx, що зберігають дані про співпадіння між двома ключовими точками, оскільки вони мають бути обрані тим же шляхом, яким використана функція matcher.match(). Наступний фрагмент коду показує, як вирівняти співпадіння у двох відповідних наборах двовимірних точок та як використати їх для пошуку фундаментальної матриці:

vector<Point2f>imgpts1,imgpts2;
for( unsigned inti = 0; i<matches.size(); i++ )
{
// queryIdx - це ліве зображення
imgpts1.push_back(keypoints1[matches[i].queryIdx].pt);
// trainIdx - це праве зображення
imgpts2.push_back(keypoints2[matches[i].trainIdx].pt);
}
Mat F = findFundamentalMat(imgpts1, imgpts2, FM_RANSAC, 0.1, 0.99,
status);
Mat_<double> E = K.t() * F * K; // згідно з Х. та З. (9.12)

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

Тепер ми готові знайти матриці камери. Процес вичерпно описаний в розділі 9 книги Х. та З.; але ми застосуємо пряму та спрощену реалізацію, а бібліотека OpenCV допоможе нам в цьому. Але спершу розглянемо спосіб побудови матриці камери.

Модель камери складається з двох елементів, компоненти повороту R та перенесення t. Цікавим фактом є те, що вона задовольняє базовому рівнянню x=PX, де x - це двовимірна точка на зображенні, а X - тривимірна у просторі. Звісно, є й інші корисні особливості, але матриця фактично встановлює важливі відношення між точками кадру й сцени. Отримавши мотивацію вирахувати її, поглянемо, як саме це зробити. Наступний фрагмент коду показує, як декомпонувати базову матрицю на елементи повороту та перенесення:

SVD svd(E);
Matx33d W(0,-1,0,//Х. та З. 9.13
1,0,0,
0,0,1);
Mat_<double> R = svd.u * Mat(W) * svd.vt; //Х. та З. 9.19
Mat_<double> t = svd.u.col(2); //ф3
Matx34d P1( R(0,0),R(0,1), R(0,2), t(0),
R(1,0),R(1,1), R(1,2), t(1),
R(2,0),R(2,1), R(2,2), t(2));

Це дуже просто. Достатньо застосувати декомпозицію єдиного значення отриманої базової матриці та помножити її на іншу, W. Не надто поглиблюючись в математичні пояснення, скажімо так: ця операція декомпонувала матрицю E на дві частини, поворот та перенесення. Власне кажучи, базову матрицю й було сформовано їх добутком. Щоб задовольнити цікавість, можна поглянути на таке рівняння, що трапляється в літературі: E=[t]xR. Вочевидь, вона складається з певного виду компоненти перенесення t та повороту R.

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

Однак, це не повне рішення. Х. та З. у своїй книзі показують чому і навіщо декомпозиція має 4 можливі матриці, але лише одна з них є справжньою. Коректною матрицею є та, що продукує реконструйовані точки з додатковим значенням Z (знаходяться перед камерою). Але ми побачимо це лише після знайомства з триангуляцією та 3D реконструкцією, що описані в наступній частині.

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

bool CheckCoherentRotation(cv::Mat_<double>& R) {
if(fabsf(determinant(R))-1.0 > 1e-07) {
cerr<<"det(R) != +-1.0, це не матриця повороту"<<endl;
return false;
}
return true;
}
Подивимось, як елементи комбінуються у функцію, що відновлює матриці P таким чином:
void FindCameraMatrices(const Mat& K,
const Mat& Kinv,
const vector<KeyPoint>& imgpts1,
const vector<KeyPoint>& imgpts2,
Matx34d& P,
Matx34d& P1,
vector<DMatch>& matches,
vector<CloudPoint>& outCloud
)
{

//Знаходимо матриці камери
//Отримуємо фундаментальну матрицюx
Mat F = GetFundamentalMat(imgpts1,imgpts2,matches);
//Базова матриця: вираховуємо та виокремлюємо камери [R|t]
Mat_<double> E = K.t() * F * K; //згідно з Х. та З. (9.12)
//декомпонуємо E в P' , згідно з Х. та З. (9.19)
SVD svd(E,SVD::MODIFY_A);
Mat svd_u = svd.u;
Mat svd_vt = svd.vt;
Mat svd_w = svd.w;
Matx33d W(0,-1,0,// Х. та З. 9.13
1,0,0,
0,0,1);
Mat_<double> R = svd_u * Mat(W) * svd_vt; //Х. та З. 9.19
Mat_<double> t = svd_u.col(2); //3
if (!CheckCoherentRotation(R)) {
cout<<"поворот не є послідовним\n";
P1 = 0;
return;
}
P1 = Matx34d(R(0,0),R(0,1),R(0,2),t(0),
R(1,0),R(1,1),R(1,2),t(1),
R(2,0),R(2,1),R(2,2),t(2));
}

На даному етапі маємо дві камери, необхідні для реконструкції сцени. Канонічна перша, що зберігається в змінній P, та обрахована друга формують фундаментальну матрицю P1. Далі пояснимо, як скористатися цими камерами для отримання 3D структури сцени.

Відновлюємо сцену

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

Час пояснити, як саме здійснювати триангуляцію за допомогою OpenCV. Цього разу слідуватимемо крокам Хартлі та Штурма, описаними в статті "Триангуляція", у якій вони реалізують та порівнюють декілька методів. Ми запрограмуємо один з лінійних способів, оскільки це можна легко зробити за рахунок можливостей бібліотеки.

Пам'ятаєте, ми мали два ключові рівняння для співпадіння двовимірних точок та матриць P: x = PX та x'=
P'X, де x та x' - це співвідносні 2D точки, а X - реальна 3D позиція, відображена двома камерами? Переписавши рівняння, сформулюємо систему, яку можна вирішити відносно невідомого значення X. Припустивши, що X = (x, y, z, 1)t (справедливо для точок, що знаходяться не надто близько та не надто далеко від центра камери) створює негомогенну лінійну систему рівнянь вигляду AX = B, можемо її закодувати та вирішити таким чином:

Mat_<double> LinearLSTriangulation(
Point3d u,//гомогенна точка зображення (u,v,1)
Matx34d P,//матриця камери 1
Point3d u1,//гомогенна точка зображення з другої камери
Matx34d P1//матриця камери 2
)
{
//будуємо матрицю A
Matx43d A(u.x*P(2,0)-P(0,0),u.x*P(2,1)-P(0,1),u.x*P(2,2)-P(0,2),
u.y*P(2,0)-P(1,0),u.y*P(2,1)-P(1,1),u.y*P(2,2)-P(1,2),
u1.x*P1(2,0)-P1(0,0), u1.x*P1(2,1)-P1(0,1),u1.x*P1(2,2)-P1(0,2),
u1.y*P1(2,0)-P1(1,0), u1.y*P1(2,1)-P1(1,1),u1.y*P1(2,2)-P1(1,2)
);
//будуємо вектор B
Matx41d B(-(u.x*P(2,3)-P(0,3)),
-(u.y*P(2,3)-P(1,3)),
-(u1.x*P1(2,3)-P1(0,3)),
-(u1.y*P1(2,3)-P1(1,3)));
//шукаємо розв'язок відносно X
Mat_<double> X;
solve(A,B,X,DECOMP_SVD);
return X;
}

В результаті отримаємо наближене положення позиції у 3D просторі, пов'язаної з парою двовимірних точок. Варто зауважити, що останні представлені гомогенними координатами, а це значить, що до значень x та y додається одиниця. Маємо впевнитись, що точки мають нормалізовані координати, тобто вони були заздалегідь помножені на матрицю калібрування K. Замість пошуку добутку кожної на матрицю K можна просто використати KP (добуток матриць K та P), як пропонують Х. та З. у розділі 9. Запрограмуємо цикл по співпадінням точок, щоб отримати повну триангуляцію так:

double TriangulatePoints(
const vector<KeyPoint>& pt_set1,
const vector<KeyPoint>& pt_set2,
const Mat&Kinv,
const Matx34d& P,
const Matx34d& P1,
vector<Point3d>& pointcloud)
{
vector<double> reproj_error;
for (unsigned int i=0; i<pts_size; i++) {
//конвертуємо в нормалізовані гомогенні координати
Point2f kp = pt_set1[i].pt;
Point3d u(kp.x,kp.y,1.0);
Mat_<double> um = Kinv * Mat_<double>(u);
u = um.at<Point3d>(0);
Point2f kp1 = pt_set2[i].pt;
Point3d u1(kp1.x,kp1.y,1.0);
Mat_<double> um1 = Kinv * Mat_<double>(u1);
u1 = um1.at<Point3d>(0);
//триангулюємо
Mat_<double> X = LinearLSTriangulation(u,P,u1,P1);
//обчислюємо похибку репроєкції
Mat_<double> xPt_img = K * Mat(P1) * X;
Point2f xPt_img_(xPt_img(0)/xPt_img(2),xPt_img(1)/xPt_img(2));
reproj_error.push_back(norm(xPt_img_-kp1));
//зберігаємо тривимірну точку
pointcloud.push_back(Point3d(X(0),X(1),X(2)));
}
//повертаємо середнє значення похибки
Scalar me = mean(reproj_error);
return me[0];
}

Далі зображено результат триангуляції на основі двох знімків фонтану, що доступні за адресою http://cvlab.epfl.ch/~strecha/multiview/denseMVS.html. У верхній частині зображено два початкові види сцени, а в нижній - вид на реконструйовану хмару точок з двох позицій, включно зі знайденими камерами, що спрямовані на фонтан. Можна бачити, як відновлено частину червоної цегляної стіни, а також вбудований фонтан.

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

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

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

Відновлюємо сцену з багатьох видів

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

Існує кілька способів коректно відновити дані тривимірної сцени з декількох видів. Перший - це зміна та оцінка пози камери, відоме як перспективна N-точка (PNP), згідно з яким намагатимемось знайти позицію на основі вже знайдених точок сцени. Інший спосіб - триангулювати більше 2D позицій та поглянути на їх взаємне розміщення на геометрії сцени: це надасть інформацію про положення нової камери за рахунок використання ітеративної найближчої точки (ICP). У цьому розділі розглянемо, як застосувати функції solvePnP бібліотеки OpenCV для реалізації першого варіанту.

На початковому кроці поступової 3D реконструкції зі зміною положення камери маємо отримати базову структуру сцени. Оскільки ми шукатимемо позицію будь-якого нового виду залежно від відомої структури сцени, варто знайти початкову структуру та базис. Можна скористатися попередньо вказаним методом - наприклад, для першого та другого кадрів - щоб отримати дані для пошуку матриць камери (з використанням функції FindCameraMatrices) та триангуляції геометрії (за допомогою TriangulatePoints).

Отримавши початкову структуру, можна продовжити процес; однак, метод вимагає дотримуватись певних обмежень. По-перше, варто зауважити, що функція solvePnP потребує два вирівнені вектори 3D та 2D точок. Це значить, що i-тий елемент в одному збігається з i-тим в іншому. Щоб віднайти їх, маємо оглянути точки навколо отриманої раніше тривимірної точки, що відноситься до двовимірної на нашому кадрі. Простий спосіб досягти цього - до кожної 3D позиції хмари доєднати вектор 2D точок, з яких вона здобута. Потім можна застосувати процес співпадіння особливих точок, щоб знайти необхідну пару.

Познайомимось з новою структурою:
struct CloudPoint {
cv::Point3d pt;
std::vector<int>index_of_2d_origin;
};
Крім власне 3D точки вона зберігає індекси двовимірних точок у відповідних векторах кожного кадра, які були враховані в процесі триангуляції. Дані index_of_2d_origin мають бути налаштовані до пошуку нової 3D точки, вказуючи на використані в процесі камери. Можна скористатися цією структурою, щоб повернутися від тривимірної точки хмари до двовимірної на кожному кадрі, таким чином:

std::vector<CloudPoint> pcloud; //глобальна хмара тривимірних точок
//шукаємо співпадіння між i-тим та 0-вим кадрами (тобто
поточною хмарою)
std::vector<cv::Point3f> ppcloud;
std::vector<cv::Point2f> imgPoints;
vector<int> pcloud_status(pcloud.size(),0);
//скануємо використані види (good_views)
for (set<int>::iterator done_view = good_views.begin(); done_view !=
good_views.end(); ++done_view)
{
int old_view = *done_view; //вже використаний для реконструкції вид
//перевіряємо на співпадіння кадри <working_view>
та <old_view> (тобто поточну хмару)
std::vector<cv::DMatch> matches_from_old_to_working =
matches_matrix[std::make_pair(old_view,working_view)];
//скануємо 2D-2D співпадіння точок
for (unsigned int match_from_old_view=0;
match_from_old_view<matches_from_old_to_working.size();
match_from_old_view++) {

// індекс співпадіння 2D точки в <old_view>
int idx_in_old_view =
matches_from_old_to_working[match_from_old_view].queryIdx;
//скануємо наявну хмару, щоб побачити, чи точка з <old_view> існує
for (unsigned int pcldp=0; pcldp<pcloud.size(); pcldp++) {
// перевіряємо, чи 2D точка з <old_view> врахована при пошуку 3D
точки хмари
if (idx_in_old_view == pcloud[pcldp].index_of_2d_origin[old_view]
&& pcloud_status[pcldp] == 0) //запобігаємо дублюванню
{
//3d точка хмари
ppcloud.push_back(pcloud[pcldp].pt);
//2d точка зображення <working_view>
Point2d pt_ = imgpts[working_view]
[matches_from_old_to_working[match_from_old_view].trainIdx].pt;
imgPoints.push_back(pt_);
pcloud_status[pcldp] = 1;
break;
}
}
}
}
cout<<"знайдено "<<ppcloud.size() <<" 3d-2d відповідностей точок"<<endl;
Тепер маємо вирівнені пари тривимірних точок сцени з урахуванням використаних двовимірних точок на кожному кадрі, тому можна використати їх для відновлення позиції камери таким чином:

cv::Mat_<double> t,rvec,R;
cv::solvePnPRansac(ppcloud, imgPoints, K, distcoeff, rvec, t, false);
//отримуємо матрицю повороту розмірності 3x3
Rodrigues(rvec, R);
P1 = cv::Matx34d(R(0,0),R(0,1),R(0,2),t(0),
R(1,0),R(1,1),R(1,2),t(1),
R(2,0),R(2,1),R(2,2),t(2));

Використовуючи функцію solvePnPRansac замість solvePnP, отримаємо менше зайвих точок. Маючи нову матрицю P1, можна просто викликати функцію TriangulatePoints, визначену раніше, та населити хмару більшою кількістю тривимірних точок. Далі, починаючи з 4-го малюнку, зображено поступову реконструкцію сцени фонтану з ресурсу http://cvlab.epfl.ch/~strecha/multiview/denseMVS.html. Верхнє ліве зображення - це результат відновлення сцени на основі 4 використаних кадрів; камери позначені червоними пірамідами та білими лініями, що вказують напрям. Решта зображень показують, як інші види формують додаткові точки хмари.

Уточнюємо реконструкцію

Одним із найважливіших етапів побудови структури з руху є уточнення та оптимізація відновленої сцени, також відоме як процес регулювання груп (BA). Це крок, на якому всі зібрані дані підлаштовуються під певну монолітну модель. Оптимізуються положення і точок, і камери, тому похибки репроєкції мінімізовані (тобто, апроксимовані 3D точки проєктуються на зображення поряд з врахованими двовимірними). Зазвичай, цей процес включає пошук розв'язків досить великих лінійних рівнянь з десятками тисяч параметрів. Можливо, це потребуватиме важкої роботи, але зроблені раніше кроки дозволять легко інтегрувати регулювання груп. Деякі речі, які раніше видавалися дивними, стануть більш зрозумілими; наприклад, причина того, чому ми зберігаємо початкові двовимірні точки для кожної тривимірної у хмарі.

Серед реалізацій відповідних алгортимів виділяють бібліотеку простого розрідженого регулювання груп (SSBA); використаємо саме його через простоту програмного інтерфейсу. Він потребує лише декілька вхідних аргументів, які можна доволі легко отримати з наявних структур даних. Перш за все нас цікавить фукнція CommonInternalsMetricBundleOptimizer, що здійснює власне оптимізацію. Їй потрібні внутрішні параметри камери, хмара тривимірних точок та відповідні їм двовимірні, спрямовані на сцену камери. Як саме отримати ці дані, вже відомо. Маємо відмітити, даний метод припускає, що всі зображення зняті одним пристроєм і мають однакові внутрішні параметри, інші способи можуть не враховувати ці обмеження. Здійснити регулювання груп можна таким чином:

void BundleAdjuster::adjustBundle(
vector<CloudPoint>&pointcloud,
const Mat&cam_intrinsics,
conststd::vector<std::vector<cv::KeyPoint>>&imgpts,
std::map<int ,cv::Matx34d>&Pmats
)
{
int N = Pmats.size(), M = pointcloud.size(), K = -1;
cout<<"камер = "<< N <<" точок = "<< M <<" вимірів =
"<< K <<endl;
StdDistortionFunction distortion;
// матриця внутрішніх параметрів
Matrix3x3d KMat;
makeIdentityMatrix(KMat);
KMat[0][0] = cam_intrinsics.at<double>(0,0);
KMat[0][1] = cam_intrinsics.at<double>(0,1);
KMat[0][2] = cam_intrinsics.at<double>(0,2);
KMat[1][1] = cam_intrinsics.at<double>(1,1);
KMat[1][2] = cam_intrinsics.at<double>(1,2);
...

// хмара 3D точок
vector<Vector3d >Xs(M);
for (int j = 0; j < M; ++j)
{
Xs[j][0] = pointcloud[j].pt.x;
Xs[j][1] = pointcloud[j].pt.y;
Xs[j][2] = pointcloud[j].pt.z;
}
cout<<"3D точки сформовані."<<endl;
// конвертуємо камери в структури для BA
vector<CameraMatrix> cams(N);
for (inti = 0; i< N; ++i)

for (inti = 0; i< N; ++i)
{
intcamId = i;
Matrix3x3d R;
Vector3d T;
Matx34d& P = Pmats[i];
R[0][0] = P(0,0); R[0][1] = P(0,1); R[0][2] = P(0,2); T[0] = P(0,3);
R[1][0] = P(1,0); R[1][1] = P(1,1); R[1][2] = P(1,2); T[1] = P(1,3);
R[2][0] = P(2,0); R[2][1] = P(2,1); R[2][2] = P(2,2); T[2] = P(2,3);
cams[i].setIntrinsic(Knorm);
cams[i].setRotation(R);
cams[i].setTranslation(T);
}
cout<<"Камери створено."<<endl;
vector<Vector2d > measurements;
vector<int> correspondingView;
vector<int> correspondingPoint;

// відповідні двовимірні точки
for (unsigned int k = 0; k <pointcloud.size(); ++k)
{
for (unsigned int i=0; i<pointcloud[k].imgpt_for_img.size(); i++) {
if (pointcloud[k].imgpt_for_img[i] >= 0) {
int view = i, point = k;
Vector3d p, np;
Point cvp = imgpts[i][pointcloud[k].imgpt_for_img[i]].pt;
p[0] = cvp.x;
p[1] = cvp.y;
p[2] = 1.0;
// Нормалізуємо результати, щоб вони відповідали одиниці фокусної довжини.
scaleVectorIP(1.0/f0, p);
measurements.push_back(Vector2d(p[0], p[1]));
correspondingView.push_back(view);
correspondingPoint.push_back(point);
}
}
} // кінець k
K = measurements.size();
cout<<"Зчитано "<< K <<" дійсних 2D вимірів."<<endl;
...
// здійснюємо регулювання груп
{
CommonInternalsMetricBundleOptimizeropt(V3D::FULL_BUNDLE_FOCAL_LENGTH_
PP, inlierThreshold, K0, distortion, cams, Xs, measurements,
correspondingView, correspondingPoint);
opt.tau = 1e-3;
opt.maxIterations = 50;
opt.minimize();
cout<<"статус оптимізатора = "<<opt.status<<endl;

}
...
//виокремлюємо 3D точки
for (unsigned int j = 0; j <Xs.size(); ++j)
{
pointcloud[j].pt.x = Xs[j][0];
pointcloud[j].pt.y = Xs[j][1];
pointcloud[j].pt.z = Xs[j][2];
}
//здобуваємо налаштовані камери
for (int i = 0; i< N; ++i)
{
Matrix3x3d R = cams[i].getRotation();
Vector3d T = cams[i].getTranslation();
Matx34d P;
P(0,0) = R[0][0]; P(0,1) = R[0][1]; P(0,2) = R[0][2]; P(0,3) = T[0];
P(1,0) = R[1][0]; P(1,1) = R[1][1]; P(1,2) = R[1][2]; P(1,3) = T[1];
P(2,0) = R[2][0]; P(2,1) = R[2][1]; P(2,2) = R[2][2]; P(2,3) = T[2];
Pmats[i] = P;
}
}

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

Використовуємо PCL для візуалізації хмари тривимірних точок

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

По-перше, маємо представити хмару (по суті, список тривимірних точок) у вигляді структур даних PCL. Це можна зробити так:
pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloud;
void PopulatePCLPointCloud(const vector<Point3d>& pointcloud,
const std::vector<cv::Vec3b>& pointcloud_RGB
)
//Населимо хмару точок
{
cout<<"Створюємо хмару точок...";
cloud.reset(new pcl::PointCloud<pcl::PointXYZRGB>);
for (unsigned int i=0; i<pointcloud.size(); i++) {
// отримуємо значення кольору RGB для точки
Vec3b rgbv(255,255,255);
if (pointcloud_RGB.size() >= i) {
rgbv = pointcloud_RGB[i];
}

// перевіряємо на наявність помилкових координат (NaN, Inf і так далі)
if (pointcloud[i].x != pointcloud[i].x || isnan(pointcloud[i].x) ||
pointcloud[i].y != pointcloud[i].y || isnan(pointcloud[i].y) ||
pointcloud[i].z != pointcloud[i].z || isnan(pointcloud[i].z) ||
fabsf(pointcloud[i].x) > 10.0 ||
fabsf(pointcloud[i].y) > 10.0 ||
fabsf(pointcloud[i].z) > 10.0) {
continue;
}
pcl::PointXYZRGB pclp;
// тривимірні координати
pclp.x = pointcloud[i].x;
pclp.y = pointcloud[i].y;
pclp.z = pointcloud[i].z;
// колір RGB, має бути представлений у вигляді цілого числа
uint32_t rgb = ((uint32_t)rgbv[2] << 16 | (uint32_t)rgbv[1] << 8 |
(uint32_t)rgbv[0]);
pclp.rgb = *reinterpret_cast<float*>(&rgb);
cloud->push_back(pclp);
}
cloud->width = (uint32_t) cloud->points.size(); // кількість точок
cloud->height = 1; // список точок, один ряд даних
}

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

void SORFilter() {
pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloud_filtered (new
pcl::PointCloud<pcl::PointXYZRGB>);
std::cerr<<"Хмара до фільтрування SOR: "<< cloud->width * cloud-
>height <<" точок даних"<<std::endl;
// Створюємо об'єкт фільтра
pcl::StatisticalOutlierRemoval<pcl::PointXYZRGB>sor;
sor.setInputCloud (cloud);
sor.setMeanK (50);
sor.setStddevMulThresh (1.0);
sor.filter (*cloud_filtered);
std::cerr<<"Хмара після фільтрування SOR: "<<cloud_filtered->width *
cloud_filtered->height <<" data points "<<std::endl;
copyPointCloud(*cloud_filtered,*cloud);
}

Тепер можемо використати програмний інтерфейс PCL для простої візуалізації хмари таким чином:
Void RunVisualization(const vector<cv::Point3d>& pointcloud,
const std::vector<cv::Vec3b>& pointcloud_RGB) {
PopulatePCLPointCloud(pointcloud,pointcloud_RGB);
SORFilter();
copyPointCloud(*cloud,*orig_cloud);
pcl::visualization::CloudViewer viewer("Огляд хмари");
// формуємо вид хмари
viewer.showCloud(orig_cloud,"оригінальна");
while (!viewer.wasStopped ())
{
// NOP
}
}

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

Використовуємо код прикладу

Приклад застосування SfM постачається разом з цією книгою. Поглянемо, як можна скомпілювати, запустити програму та скористатися її виводом. Для цього знадобиться CMake, кросплатформове середовище побудови, подібне до Maven чи SCons. Також необхідно забезпечити виконання таких вимог:
OpenCV v2.3 чи вище
PCL v1.6 чи вище
SSBA v3.0 чи вище

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

Маємо впевнитися, що CMake знаходить SSBA та PCL. Якщо PCL встановлено правильно, проблем бути не має; однак, потрібно встановити коректне розміщення встановленої бібліотеки SSBA як значення параметра побудови DSSBA_LIBRARY_DIR. Якщо операційною системою є Windows, для побудови можна використати Microsoft Visual Studio; для цього виконаємо таку команду:
cmake –G "Visual Studio 10" -DSSBA_LIBRARY_DIR=../3rdparty/SSBA3.0/build/ ..
Якщо ж працюємо в Linux, Mac OS чи іншій Unix-подібній ОС, матимемо наступну стрічку:
cmake –G "Unix Makefiles" -DSSBA_LIBRARY_DIR=../3rdparty/SSBA3.0/build/ ..
Для XCode та Mac OS команда така:
cmake –G Xcode -DSSBA_LIBRARY_DIR=../3rdparty/SSBA-3.0/build/ ..

CMake також здатна створювати макроси для Eclipse, Codeblocks та інших IDE. Коли закінчиться формування середовища, приступимо й до побудови рішення. В Unix-подібній системі можна просто скористатися утилітою make, в інших випадках достатньо застосувати процес побудови певного середовища розробки. Коли побудова завершиться, в результаті отримаємо файл ExploringSfMExec, який покаже в дії процес SfM. Запустивши його без аргументів, отримаємо наступну стрічку: USAGE: ./ExploringSfMExec <path_to_images>

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

Висновки

У цьому розділі ми побачили, яким саме чином OpenCV допомагає побудувати структуру з руху в спосіб, який легко закодувати та зрозуміти. Програмний інтерфейс бібліотеки включає набір корисних функцій та структур даних, які полегшують життя та спрощують реалізацію.
Однак, шедевральні методи SfM набагато складніші. Багато нюансів ми відкидаємо заради простоти, а ще ж необхідні інші перевірки на помилки. До того ж, обрані методи для різних елементів SfM можна удосконалити. Наприклад, Х. та З. пропонують точний метод триангуляції, який мінімізує похибки репроєкції. Деякі з них використовують навіть N-видову триангуляцію у разі появи зв'язків між особливими точками на декількох зображеннях.

Якщо потрібно розширити та поглибити знання про відновлення структури з руху, можна відшукати їх в інших відкритих тематичних бібліотеках. Особливо цікавим проєктом є libMV, що реалізує широкий масив елементів SfM, взаємозамінних задля кращих результатів. Існує також декілька праць Університету Вашингтона, в яких описані інструменти для багатьох сфер SfM (Bundler та VisualSfM). Ця праця надихнула розробників онлайн продукту Microsoft під назвою PhotoSynth. В мережі доступні й інші реалізації SfM, достатньо лише віднайти їх.

Ще одним важливим фактом, який ми не обговорювали раніше, є взаємозв'язок SfM та VLM, краще відомий як методи одночасної локалізації та відображення (SLAM). У цьому розділі ми мали справу із заданим набором зображень та послідовністю відеокадрів, застосування SfM доречне в таких випадках; але деякі програми не мають попередньо записаних наборів даних та мають проводити реконструкцію в режимі реального часу. Цей процес відомий як відображення, він застосований для створення 3D карти світу на основі співпадіння ключових точок, відслідковування у 2D просторі, та триангуляції.
У наступному розділі побачимо, як використати OpenCV для пошуку ліцензованих номерних пластин на зображеннях, застосовуючи різні техніки машинного навчання.

Посилання
Multiple View Geometry in Computer Vision, Richard Hartley and Andrew Zisserman,
Cambridge University Press
Triangulation, Richard I. Hartley and Peter Sturm, Computer vision and image
understanding, Vol. 68, pp. 146-157
http://cvlab.epfl.ch/~strecha/multiview/denseMVS.html
On Benchmarking Camera Calibration and Multi-View Stereo for High Resolution
Imagery,C. Strecha, W. von Hansen, L. Van Gool, P. Fua, and U. Thoennessen, CVPR
http://www.inf.ethz.ch/personal/chzach/opensource.html
http://www.ics.forth.gr/~lourakis/sba/
http://code.google.com/p/libmv/
http://www.cs.washington.edu/homes/ccwu/vsfm/
http://phototour.cs.washington.edu/bundler/
http://photosynth.net/
http://en.wikipedia.org/wiki/Simultaneous_localization_and_mapping
http://pointclouds.org
http://www.cmake.org

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

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

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

Настройки

Готово:

100.00% КП = 1.0