Mastering OpenCV with Practical Computer Vision Projects / Опановуюємо OpenCV через практичні проекти комп'ютерного зору: Розділ 2. Доповнена реальність на iOS та Android з використанням маркерів
| Английский оригинал | Перевод на украинский | |
|---|---|---|
Сожалеем, но текст оригинала доступен только зарегистрированным пользователям. |
Зверніть увагу на код обробки помилок. Для настільки важливих процесів, як налаштування апаратного забезпечення, повернення продуманих значень є хорошою практикою. Без цього виконання коду може неочікувано перерватися, не оповістивши користувача про причини. Atronax 17.02.21 в 08:23 |
|
|
Ми створили сесію захоплення та додали джерело відео. Настав час приєднати об'єкт, що отримуватиме власне дані зображень. Для обробки нестиснутих кадрів відеопотоку використовується клас AVCaptureVideoDataOutput. Камера здатна забезпечувати зображеннями у форматі BGRA, CMYK чи простій моделі у відтінках сірого. Для наших цілей найкраще підходить BGRA, оскільки цей варіант дозволяє легко показувати та оброблювати кадри. Наступний код належить функції addRawViewOutput: Atronax 17.02.21 в 08:36 |
||
|
- (void) addRawViewOutput Atronax 17.02.21 в 08:42 |
||
|
// Встановлюємо зберігання кадрів у форматі BGRA (це має підвищити Atronax 17.02.21 в 08:45 |
||
|
Сесія захоплення нарешті налаштована. Після запуску вона отримуватиме кадри камери та відправлятиме їх у код користувача. Коли маємо нове доступне зображення, об'єкт AVCaptureSession активує функцію зворотного виклику captureOutput: didOutputSampleBuffer:fromConnection. Тут здійснюємо незначні операції над виглядом даних, щоб отримати більш зручний формат, та передаємо їх далі. Atronax 17.02.21 в 08:52 |
||
|
- (void)captureOutput:(AVCaptureOutput *)captureOutput Atronax 17.02.21 в 08:55 |
||
|
Ми отримали посилання на буфер зображень, що зберігає дані кадру. Потім здійснили блокування, аби нові зображення не модифікували його. Відтепер маємо виключний доступ до об'єкта. Використовуючи програмний інтерфейс CoreVideo, отримуємо розміри кадру, протяжність (кількість пікселів у рядку) та вказівник на початок даних. Atronax 17.02.21 в 09:00 |
||
|
Примітка Atronax 17.02.21 в 09:11 |
||
|
Розпізнавання маркера Atronax 17.02.21 в 09:12 |
||
|
У більшості випадків маркер виглядає як прямокутне зображення з чорними та білими зонами всередині. Зважаючи на відомі обмеження, процедура розпізнавання є досить простою. Перш за все, необхідно знайти закриті контури вхідного кадру, розгорнути внутрішнє зображення у прямокутник та порівняти його з моделлю маркера. Atronax 17.02.21 в 09:16 |
||
|
Для цього прикладу використаємо маркер з розмірами 5х5. Він виглядає так: Atronax 17.02.21 в 09:24 |
||
|
/** Atronax 17.02.21 в 09:31 |
||
|
protected: Atronax 17.02.21 в 09:32 |
||
|
Щоб ви краще зрозуміли процедуру розпізнавання маркера, наведемо покроковий алгоритм обробки одного кадра відео. Як приклад буде використано вхідне зображення, зняте камерою iPad: Atronax 17.02.21 в 09:36 |
||
|
Ідентифікація маркера Atronax 17.02.21 в 09:37 |
||
|
Ось робочий потік процедури розпізнавання маркера: Atronax 17.02.21 в 09:39 |
||
|
Конвертація у відтінки сірого Atronax 17.02.21 в 09:39 |
||
|
Перетворення у відтінки сірого важливе, адже в більшості випадків маркери складаються з чорних та білих блоків, а ними зручніше оперувати на конвертованих кадрах. На щастя, зміна простору кольорів в OpenCV доволі проста. Atronax 17.02.21 в 09:44 |
||
|
Погляньте на наступний фрагмент коду C++: Atronax 17.02.21 в 09:46 |
||
|
Ця функція перетворить вхідне зображення BGRA у відтінки сірого (за необхідності буде виділено буфери кадрів) та використає результат як другий аргумент. Усі подальші кроки здійснимо на конвертованих кадрах. Atronax 17.02.21 в 09:51 |
||
|
Бінаризація зображення Atronax 17.02.21 в 09:52 |
||
|
Операція бінаризації змінить колір кожного пікселя зображення на чорний (з нульовою інтенсивністю) чи білий (з повною). Цей крок необхідний для пошуку контурів. Існує декілька порогових методів; кожен має сильні та слабкі сторони. Найлегший та найшвидший метод - це абсолютний поріг, у якому результат залежить від інтенсивності поточного пікселя та деякого значення порогу. Якщо інтенсивність пікселя більша за значення порогу, результат буде білим (255); інакше чорним (0). Atronax 17.02.21 в 10:01 |
||
|
Цей спосіб має величезний недолік - він залежить від умов освітлення та незначних змін інтенсивності. До кращих методів відносять адаптивний поріг. Головна його відмінність - це використання усіх пікселів в певному радіусі навколо поточного. Використання середньої інтенсивності дає прийнятні результати та дозволяє знаходити більш точні кути. Atronax 17.02.21 в 10:18 |
||
|
Наступний фрагмент коду належить функції MarkerDetector: Atronax 17.02.21 в 10:20 |
||
|
Після застосування адаптивного порогу до вхідного зображення, результат стає схожим на наступне: Atronax 17.02.21 в 10:22 |
||
|
Розпізнавання контурів Atronax 17.02.21 в 10:22 |
||
|
Функція cv::findCountours розпізнає контури на вхідному бінарному зображенні: Atronax 17.02.21 в 10:24 |
||
|
Ця функція поверне список полігонів, кожен з яких представляє єдиний контур. Вона пропускає ті з них, кількість точок яких менша за значення змінної minContourPointsAllowed. Це так, бо ми не зацікавлені в малих контурах. (Скоріше за все, вони не включатимуть маркери або контур не вдасться розпізнати через малі розміри.) Наступний малюнок показує етап візуалізації знайдених контурів: Atronax 17.02.21 в 10:30 |
||
|
Пошук кандидатів Atronax 17.02.21 в 10:30 |
||
|
Після знаходження контурів виконується етап апроксимації полігону. Це робиться для зниження кількості точок, що описують форму контуру. Це також непоганий контроль якості, що дозволяє відфільтрувати зони без маркерів, адже останні завжди можна представити у вигляді полігонів з чотирма вершинами. Якщо апроксимований полігон має більше або менше ніж чотири вершини, вочевидь, нам він непотрібний. Наступний код реалізує цю ідею: Atronax 17.02.21 в 10:35 |
||
|
void MarkerDetector::findCandidates Atronax 17.02.21 в 14:58 |
||
|
// Ми зацікавлені лише в полігонах, що складаються з 4 точок Atronax 17.02.21 в 17:21 |
||
|
// Перевіряємо, що відстань не надто мала Atronax 17.02.21 в 17:26 |
||
|
std::swap(m.points[1], m.points[3]); Atronax 17.02.21 в 17:30 |
||
|
float distSquared = 0; Atronax 17.02.21 в 17:31 |
||
|
// Помітимо на видалення елемент пари з меншим периметром Atronax 17.02.21 в 17:32 |
||
|
// Повертаємо кандидатів Atronax 17.02.21 в 17:32 |
||
|
Відтепер маємо список паралелепіпедів, які з певною мірою вірогідності представлятимуть маркери. Щоб підтвердити приналежність, здійснюємо три кроки: Atronax 17.02.21 в 17:50 |
||
|
Щоб отримати прямокутний маркер, потрібно розгорнути вхідне зображення з використанням перспективних перетворень. Необхідну матрицю можна вирахувати за допомогою функції cv::getPerspectiveTransform. Вона знаходить перетворення для 4 пар відповідних точок. Перший аргумент - це координати маркера у просторі зображення, а другий відповідає координатам квадратного фрагмента маркера. Знайдені перетворення нададуть маркеру квадратної форми та дозволять його проаналізувати: Atronax 17.02.21 в 18:01 |
||
|
Розгортання зображення надає йому прямокутної форми за допомогою перетворень перспективи: Atronax 17.02.21 в 18:06 |
||
|
Розпізнавання кода маркера Atronax 17.02.21 в 18:07 |
||
|
Кожен маркер має внутрішній код, описаний 5 словами по 5 бітів в кожному. Застосований шифр є дещо зміненим кодом Хеммінга. В загальному кожне слово має 2 біти корисної інформації з 5 використаних. Решта 3 потрібні для розпізнавання помилок. Як результат, можна отримати до 1024 різних ідентифікаторів. Atronax 17.02.21 в 18:15 |
||
|
Головна відмінність від базового коду - це те, що перший біт (паритет 3 та 5) інвертовано. Тому ідентифікатор 0 (що описується 5 нулями) стає 10000 в нашому коді. Завдяки цьому можна запобігти явищу, коли повністю чорний маркер виявляється дійсним ідентифікатором. Це необхідно, щоб зменшити кількість хибних появ серед об'єктів середовища. Atronax 17.02.21 в 18:30 |
||
|
Підрахунок кількості чорних та білих пікселів для кожного блока дає маску розмірності 5х5 з кодом маркера. Щоб знайти число ненульових пікселів на певному зображенні, застосовано функцію cv::countNonZero. Вона рахує ненульові елементи заданого одно- чи двовимірного масиву. Клас cv::Mat здатний повертати новий екземпляр, що включає частину початкового зображення. Наприклад, якщо існує cv::Mat з розмірами 400х400, наступний фрагмент коду створить підматрицю для блоку зображення з координатами (:10,10) та розмірами 50х50: Atronax 17.02.21 в 18:43 |
||
|
Зчитування коду маркера Atronax 17.02.21 в 18:43 |
||
|
Використовуючи цю техніку, можна знайти блоки на поверхні маркера: Atronax 17.02.21 в 18:46 |
||
|
Зверніть увагу на наступний малюнок. Той же маркер може мати чотири можливі представлення залежно від точки зору камери: Atronax 17.02.21 в 18:47 |
||
|
Оскільки існує 4 можливі орієнтації малюнку маркера, потрібно знайти коректну позицію маркера. Пам'ятайте, для кожних 2 бітів інформації доступні 3 для перевірки. Завдяки їм можна знайти відстань Хеммінга для кожної можливої орієнтації маркера. Правильна позиція матиме нульову похибку відстані, а інші варіанти повороту - ні. Atronax 17.02.21 в 18:53 |
||
|
Далі розміщено код, що повертає бітову матрицю 4 рази та знаходить вірну орієнтацію маркера: Atronax 17.02.21 в 18:56 |
||
|
Цей код знаходить орієнтацію бітової матриці таким чином, щоб мати мінімальну похибку для вимірів відстані Хеммінга. Вона має бути нульовою для коректного ідентифікатора маркера; якщо це не так, ми маємо невірний шаблон маркера (зіпсоване зображення чи розпізнавання хибних появ). Atronax 17.02.21 в 19:01 |
