Mastering OpenCV with Practical Computer Vision Projects / Опановуюємо OpenCV через практичні проекти комп'ютерного зору: Розділ 2. Доповнена реальність на iOS та Android з використанням маркерів

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

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

Обробка місцеположення маркера

Atronax 18.02.21 в 07:41

0

Визначивши вірну орієнтацію маркера, повертаємо його кути так, щоб забезпечити правильний порядок:
// сортуємо точки, щоб їхній порядок
// не залежав від орієнтації камери
std::rotate(marker.points.begin(), marker.points.begin() + 4 -
nRotations,
marker.points.end());

Atronax 18.02.21 в 07:44

0

Після розпізнавання маркера та декодування ідентифікатора, обробимо його кути. Ця операція знадобиться, коли ми оцінюватимемо положення маркера у тривимірному просторі. Щоб знайти положення точки з субпіксельною точністю, використаємо функцію cv::cornerSubPix:
std::vector<cv::Point2f> preciseCorners(4 * goodMarkers.size());
for (size_t i=0; i<goodMarkers.size(); i++)
{
Marker& marker = goodMarkers[i];
for (int c=0;c<4;c++)
{
preciseCorners[i*4+c] = marker.points[c];
}
}
cv::cornerSubPix(grayscale, preciseCorners, cvSize(5,5), cvSize(-1,-
1), cvTermCriteria(CV_TERMCRIT_ITER,30,0.1));
//copy back
for (size_t i=0;i<goodMarkers.size();i++)
{
Marker&marker = goodMarkers[i];
for (int c=0;c<4;c++)
{
marker.points[c] = preciseCorners[i*4+c];
}
}

Atronax 18.02.21 в 07:49

0

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

Atronax 18.02.21 в 07:55

0

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

Atronax 18.02.21 в 08:00

0

Розміщуємо маркер у тривимірному просторі

Atronax 18.02.21 в 08:00

0

Доповнена реальність намагається об'єднати дійсний об'єкт з віртуальним контентом. Щоб помістити 3D-модель на сцені, потрібно знати її позу відповідно до камери, використаної для захоплення відеокадрів. Скористаємось евклідовими перетвореннями в прямокутній системі координат, щоб представити її. Позиція маркера в тривимірному та відповідна проєкція у двовимірному просторі обмежена наступним рівнянням:
P = A * [R|T] * M,
де:
M позначає точку в 3D просторі,
[R|T] стосується матриці [3|4] евклідових перетворень,
A описує матрицю камери або матрицю внутрішніх параметрів,
P відповідає проєкції M в екранних координатах.

Atronax 18.02.21 в 08:09

0

Виконавши крок розпізнавання маркера, ми отримаємо чотири кути маркера у двовимірному просторі (проєкція в екранних координатах). У наступній частині ви дізнаєтесь, як знайти матрицю A та вектор параметрів M, а також як обрахувати перетворення [R|T].

Atronax 18.02.21 в 08:12

0

Калібрування камери

Atronax 18.02.21 в 08:12

0

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

Atronax 18.02.21 в 08:21

0

Для калібрування знадобиться особливе зображення шаблону (шахова дошка чи чорні кола на білому фоні). Камера, над якою здійснюють цей процес, робить 10-15 знімків шаблону з різних точок зору. Потім алгоритм калібрування знаходить оптимальні внутрішні параметри та вектор викривлень:

Atronax 18.02.21 в 08:24

0

Для опису калібрування камери використаємо клас CameraCalibration:
/**
* Клас калібрування, що зберігає внутрішню матрицю та
коефіцієнти викривлення.
*/
class CameraCalibration
{
public:
CameraCalibration();
CameraCalibration(float fx, float fy, float cx, float cy);
CameraCalibration(float fx, float fy, float cx, float cy, float
distorsionCoeff[4]);
void getMatrix34(float cparam[3][4]) const;
const Matrix33& getIntrinsic() const;
const Vector4& getDistorsion() const;
private:
Matrix33 m_intrinsic;
Vector4 m_distorsion;
};

Atronax 18.02.21 в 08:31

0

Детальне пояснення процедури калібрування лежить за межами цього розділу. Зверніть увагу на приклад camera_calibration чи статтю "OpenCV: оцінка відношень проєкції на зображеннях" за адресою http://www.packtpub.com/article/opencv-estimating-projective-relations-images, щоб знайти додаткову інформацію чи код.

Atronax 18.02.21 в 08:35

0

Що ж до цього прикладу, подамо внутрішні параметри для всіх сучасних пристроїв iOS (iPad2, iPad3 та iPhone 4).

Atronax 18.02.21 в 08:43

0

Оцінка пози маркера

Atronax 18.02.21 в 08:43

0

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

Atronax 18.02.21 в 08:50

0

Точка С призначена позначати центр камери. P1-P4 - це тривимірні точки у світовій координатній системі, а p1-p4 - їхні проєкції на площину зображення. Ми маємо знайти відносні перетворення між відомою позицією маркера в 3D і камерою C, що використовує матрицю внутрішнього стану, та відомими проєкціями на поверхні зображення. Але ж як ми знайдемо координати позиції маркера у тривимірному просторі? Уявимо. Маркер завжди має квадратну форму, а вершини лежать в одній площині, тому кути можна визначити так:

Atronax 18.02.21 в 08:59

0

Помістимо маркер на площину XY (компонента Z рівна нулю), причому центр маркера відповідатиме точці (0.0, 0.0, 0.0). Це необхідний крок, адже в такому випадку координатна система матиме початок в центрі маркера (вісь Z перпендикулярна площині маркера).
Щоб знайти положення камери за відомими відношеннями 2D-3D, можна використати функцію cv::solvePnP:
void solvePnP(const Mat& objectPoints, const Mat& imagePoints, const
Mat&
cameraMatrix, const Mat& distCoeffs, Mat& rvec, Mat& tvec, bool
useExtrinsicGuess=false);

Atronax 18.02.21 в 09:04

0

Параметр objectPoints - це вхідний масив точок об'єкта у локальній площині координат. Це може бути, наприклад, std::vector<cv::Point3f>. Матрицю OpenCV з розмірами 3xN чи Nx3, де N - це кількість точок, також можна передати як вхідний аргумент. Тут ми передаємо список координат маркера в тривимірному просторі (вектор 4 точок).

Atronax 18.02.21 в 09:13

0

Об'єкт imagePoints - це масив відповідних точок зображення (чи проєкцій). Також аргумент може мати тип std::vector<cv::Point2f> чи cv::Mat з розмірами 2xN чи Nx2, де N - це кількість точок.

Atronax 18.02.21 в 10:04

0

Сюди передаємо список знайдених кутів маркера.
cameraMatrix - це матриця 3x3 внутрішнього стану камери.
distCoeffs - вхідний вектор викривлень (k1, k2, p1, p2, [k3]) розмірності 4x1, 1x4, 5x1 чи 1x5; якщо він набуває значення NULL, всі коефіцієнти дорівнюють 0.
rvec - вихідний вектор поворотів, що разом з tvec переносять точки з локальних координат у систему координат камери.
tvec - вихідний вектор перенесень.
useExtrinsicGuess: якщо рівна true, функція використає вектори rvec та tvec як початкові наближення для повороту та перенесення та пізніше оптимізує їх.

Atronax 18.02.21 в 10:33

0

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

Atronax 18.02.21 в 10:43

0

Вирахувані перетворення визначені поворотом (rvec) та перенесенням (tvec). Вони також відомі як евклідові або ортогональні.

Atronax 18.02.21 в 10:47

0

Евклідові перетворення формально визначено як такі, що перетворюють будь-який вектор v на інший, T(v), що має форму:
T(v) = Rv + t,
де RT = R-1 (тобто R є ортогональним перетворенням), а t - це вектор, що визначає перенесення початку координат. Крім того, коректне евклідове перетворення має
det(R) = 1.

Atronax 18.02.21 в 10:53

0

Це значить, що R не формує віддзеркалень і, таким чином, визначає поворот (ортогональне перетворення, що зберігає орієнтацію). Для пошуку матриці розмірності 3x3 з вектора застосовано функцію cv::Rodrigues. Вона конвертує поворот, представлений вектором, та повертає еквівалентну матрицю.

Atronax 18.02.21 в 11:03

0

Примітка
Через те, що функція cv::solvePnP шукає позицію камери відносно пози маркера у тривимірному просторі, ми маємо інвертувати знайдені перетворення. Результат опише перетворення у системі координат камери, що значно спрощує подальший процес візуалізації.

Atronax 18.02.21 в 11:08

0

Це лістинг функції estimatePosition, яка шукає позицію виявлених маркерів:
void MarkerDetector::estimatePosition(std::vector<Marker>&
detectedMarkers)
{
for (size_t i=0; i<detectedMarkers.size(); i++)
{
Marker& m = detectedMarkers[i];
cv::Mat Rvec;
cv::Mat_<float> Tvec;
cv::Mat raux,taux;
cv::solvePnP(m_markerCorners3d, m.points, camMatrix,
distCoeff,raux,taux);
raux.convertTo(Rvec,CV_32F);
taux.convertTo(Tvec ,CV_32F);
cv::Mat_<float> rotMat(3,3);
cv::Rodrigues(Rvec, rotMat);
// Копіюємо в матрицю перетворень
m.transformation = Transformation();
for (int col=0; col<3; col++)
{
for (int row=0; row<3; row++)
{
// Копіюємо компоненту повороту
m.transformation.r().mat[row][col] = rotMat(row,col);
}
// Копіюємо компоненту перенесення
m.transformation.t().data[col] = Tvec(col);
}
// Оскільки solvePnP шукає положення камери відносно пози маркера,
// щоб отримати позу маркера відносно камери, інвертуємо її.
m.transformation = m.transformation.getInverted();
}

Atronax 18.02.21 в 11:12

0

Візуалізуємо тривимірний віртуальний об'єкт

Atronax 18.02.21 в 11:13

0

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

Atronax 18.02.21 в 11:18

0

Примітка
Існує велика кількість комерційних та відкритих 3D-двигунів (Unity, Unreal Engine, Ogre і так далі). Але усі вони використовують OpenGL або DirectX, щоб передавати команди відеокарті. DirectX - це власний програмний інтерфейс Microsoft та підтримується лише на платформі Windows. Саме тому OpenGL - єдиний кандидат для програмування кросплатформових систем візуалізації.

Atronax 18.02.21 в 11:22

0

Розуміння принципів систем візуалізації наділить вас необхідним досвідом та знаннями, необхідними для використання цих двигунів чи створення власних.

Atronax 18.02.21 в 11:23

0

Створюємо рівень візуалізації OpenGL

Atronax 18.02.21 в 11:23

0

Щоб використати функції OpenGL у своєму застосунку, необхідно отримати контекстну поверхню графіки iOS, яка презентуватиме візуалізовану сцену. Частіше за все, контекст прив'язано до виду, який спостерігає користувач. Наступний скріншот показує ієрархію інтерфейсу програми в інструменті XCode для їх побудови:

Atronax 18.02.21 в 11:30

0

Інкапсулюємо логіку налаштування контексту OpenGL у класі EAGLView:
@class EAGLContext;
// Цей клас надбудовує CAEAGLLayer з CoreAnimation у
зручному підкласі UIView.
// Контент виду - це в основному поверхня EAGL, на якій зображено
сцену OpenGL.
// Врахуйте, що налаштування прозорості виду спрацює лише за умови,
що поверхня EAGL має альфа-канал.
@interface EAGLView : UIView
{
@private
// Для візуалізації у вид використано імена
OpenGL ES для обох
необхідних буферів.
GLuint defaultFramebuffer, colorRenderbuffer;
}
@property (nonatomic, retain) EAGLContext *context;
// The pixel dimensions of the CAEAGLLayer.
@property (readonly) GLint framebufferWidth;
@property (readonly) GLint framebufferHeight;
- (void)setFramebuffer;
- (BOOL)presentFramebuffer;
- (void)initContext;
@end

Atronax 18.02.21 в 11:38

0

Цей клас під'єднується до нашого виду у файлі визначень інтерфейсів, тому, коли завантажується файл NIB, потік виконання створить новий екземпляр класу EAGLView. Він прийматиме події від iOS та налаштує контекст візуалізації OpenGL.

Atronax 18.02.21 в 12:16

0

Далі розміщено код функції initWithCoder:
//Вид EAGL зберігається у файлі nib. Його зчитують та відправляють в -initWithCoder:.
- (id)initWithCoder:(NSCoder*)coder
{
self = [super initWithCoder:coder];
if (self) {
CAEAGLLayer *eaglLayer = (CAEAGLLayer *)self.layer;
eaglLayer.opaque = TRUE;
eaglLayer.drawableProperties = [NSDictionary
dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:FALSE],
kEAGLDrawablePropertyRetainedBacking,
kEAGLColorFormatRGBA8,
kEAGLDrawablePropertyColorFormat,
nil];
[self initContext];
}
return self;
}

Atronax 18.02.21 в 12:22

0

- (void)createFramebuffer
{
if (context && !defaultFramebuffer) {
[EAGLContext setCurrentContext:context];
// Створюємо буфер кадрів за замовчуванням.
glGenFramebuffers(1, &defaultFramebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebuffer);
// Створюємо буфер візуалізації та виділяємо місце для збереження.
glGenRenderbuffers(1, &colorRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, colorRenderbuffer);
[context renderbufferStorage:GL_RENDERBUFFER fromDrawable:
(CAEAGLLayer *)self.layer];
glGetRenderbufferParameteriv(GL_RENDERBUFFER,
GL_RENDERBUFFER_WIDTH, &framebufferWidth);
glGetRenderbufferParameteriv(GL_RENDERBUFFER,
GL_RENDERBUFFER_HEIGHT, &framebufferHeight);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_RENDERBUFFER, colorRenderbuffer);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) !=
GL_FRAMEBUFFER_COMPLETE)
NSLog(@"Не вдалося створити повний об'єкт буферу кадрів %x",
glCheckFramebufferStatus(GL_FRAMEBUFFER));
//glClearColor(0, 0, 0, 0);
NSLog(@"Буфер кадрів створено");
}
}

Atronax 18.02.21 в 12:24

0

Візуалізуємо сцену доповненої реальності

Atronax 18.02.21 в 12:25

0

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

Atronax 18.02.21 в 12:33

0

Для візуалізації доповненої реальності створимо окремий клас з назвою
VisualizationController:
@interface SimpleVisualizationController :
NSObject<VisualizationController>
{
EAGLView * m_glview;
GLuint m_backgroundTextureId;
std::vector<Transformation> m_transformations;
CameraCalibration m_calibration;
CGSize m_frameSize;
}
-(id) initWithGLView:(EAGLView*)view calibration:(CameraCalibration)
calibration frameSize:(CGSize) size;
-(void) drawFrame;
-(void) updateBackground:(BGRAVideoFrame) frame;
-(void) setTransformationList:(const std::vector<Transformation>&)
transformations;

Atronax 18.02.21 в 12:35

0

Функція drawFrame виконує візуалізацію доповненої реальності на заданий цільовий вид EAGLView. Він включає наступні кроки:
1. Очищує сцену.
2. Налаштовує ортографічну проєкцію для зображення фону.
3. Малює останній отриманий з камери кадр в області виду.
4. Налаштовує перспективну проєкцію із врахуванням внутрішніх параметрів камери.
5. Для кожного визначеного маркера переміщує систему координат у тривимірну позицію маркера (передає матрицю перетворень розмірності 4x4 як матрицю моделі-виду OpenGL).
6. Візуалізує власне тривимірний об'єкт.
7. Показує буфер кадру.

Atronax 18.02.21 в 12:43

0

Функцію drawFrame викликаємо, коли кадр повністю готовий для малювання. Це відбувається, коли зображення з камери вже надійшло до відеопам'яті, а етап розпізнавання маркера завершено. Наступний код належить цій функції:

Atronax 18.02.21 в 12:52

0

- (void)drawFrame
{
// Встановлюємо активний буфер кадру
[m_glview setFramebuffer];
// Малюємо відео на фоні
[self drawBackground];
// Малюємо 3D-об'єкти в позиції знайдених маркерів
[self drawAR];
// Презентуємо буфер кадру
bool ok = [m_glview presentFramebuffer];
int glErCode = glGetError();
if (!ok || glErCode != GL_NO_ERROR)
{
std::cerr << "Виявлено помилку GL. Код останньої:" << glErCode <<
std::endl;
}
}

Atronax 18.02.21 в 12:55

0

Намалювати фон доволі легко; ми встановлюємо ортографічну проєкцію та виводимо повноекранну текстуру, створену із зображення поточного кадра. Ось лістинг коду, що використовує GLES 1 API з цією метою:

Atronax 18.02.21 в 12:57

0

- (void) drawBackground
{
GLfloat w = m_glview.bounds.size.width;
GLfloat h = m_glview.bounds.size.height;
const GLfloat squareVertices[] =
{
0, 0,
w, 0,
0, h,
w, h
};
static const GLfloat textureVertices[] =
{
1, 0,
1, 1,
0, 0,
0, 1
};
static const GLfloat proj[] =
{
0, -2.f/w, 0, 0,
-2.f/h, 0, 0, 0,
0, 0, 1, 0,
1, 1, 0, 1
};
glMatrixMode(GL_PROJECTION);
glLoadMatrixf(proj);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glDisable(GL_COLOR_MATERIAL);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, m_backgroundTextureId);
// Оновлюємо значення атрибутів.
glVertexPointer(2, GL_FLOAT, 0, squareVertices);
glEnableClientState(GL_VERTEX_ARRAY);
glTexCoordPointer(2, GL_FLOAT, 0, textureVertices);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glColor4f(1,1,1,1);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
glDisable(GL_TEXTURE_2D);
}

Atronax 18.02.21 в 12:58

0

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

Atronax 18.02.21 в 13:02

0

Це фрагмент коду, що формує матрицю проєкції OpenGL з внутрішніх характеристик камери:
- (void)buildProjectionMatrix:(Matrix33)cameraMatrix:
(int)screen_width: (int)screen_height: (Matrix44&) projectionMatrix
{
float near = 0.01; // Ближня відстань відсічення
float far = 100; // Дальня відстань відсічення
// Параметри камери
float f_x = cameraMatrix.data[0]; // Фокусна відстань по осі x
float f_y = cameraMatrix.data[4]; // Фокусна відстань по осі y (звичайно така ж?)
float c_x = cameraMatrix.data[2]; // Координата x кардинальної точки
float c_y = cameraMatrix.data[5]; // Координата у кардинальної точки

Atronax 18.02.21 в 13:10

0

projectionMatrix.data[0] = - 2.0 * f_x / screen_width;
projectionMatrix.data[1] = 0.0;
projectionMatrix.data[2] = 0.0;
projectionMatrix.data[3] = 0.0;
projectionMatrix.data[4] = 0.0;
projectionMatrix.data[5] = 2.0 * f_y / screen_height;
projectionMatrix.data[6] = 0.0;
projectionMatrix.data[7] = 0.0;
projectionMatrix.data[8] = 2.0 * c_x / screen_width - 1.0;
projectionMatrix.data[9] = 2.0 * c_y / screen_height - 1.0;
projectionMatrix.data[10] = -( far+near ) / ( far - near );
projectionMatrix.data[11] = -1.0;
projectionMatrix.data[12] = 0.0;
projectionMatrix.data[13] = 0.0;
projectionMatrix.data[14] = -2.0 * far * near / ( far - near );
projectionMatrix.data[15] = 0.0;
}

Atronax 18.02.21 в 13:10

0

Під'єднавши останню до потоку роботи OpenGL, намалюємо декілька об'єктів. Кожне перетворення можна описати матрицею розмірності 4x4 та завантажити у внутрішню бібліотеки, моделі-виду. Це перемістить початок координат у позицію маркера у світовій системі.

Atronax 18.02.21 в 13:19

0

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

Atronax 18.02.21 в 14:01

0
Минутку...

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

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