Pull to refresh

2D->3D in Augmented reality

AR and VR
Sandbox
image

В данной статье я расскажу как в приложениях Augmented reality по найденому расположению объекта в сцене построить 3D-пространство. Для этого необходимо получить две матрицы – проекционную (GL_PROJECTION) и модельную (GL_MODELVIEW) для работы, например, в OpenGL. Делать это мы будем средствами библиотеки OpenCV.

Недавно приходилось решать эту задачу, но ресурса, где просто поэтапно объяснялось как это сделать я не нашел (может плохо искал), а подводных камней в данной проблеме хватает. В любом случае, статья на хабре описывающая эту задачу не повредит.

Вступление


Сам я — iOS программист, в свободное время занимаюсь разработкой собственного движка Augmented Reality. За базу взял OpenCV – библиотека с открытым кодом для компьютерного зрения.

Вообще, наиболее интересным предложением для мобильных устройств (iOS/Android) в данной области являются разработки компании Qualcomm.

Сравнительно недавно, они выпустили собственный AR SDK под именем Vuforia. При этом, пользование SDK является бесплатным как для разработки, так и для выкладывании приложения в магазин (AppStore, AndroidMarket), о чем гордо заявляет абзац Licensing. В то же время, они пишут что вы должны предупредить конечного пользователя, о том, что данное SDK может собирать некоторую анонимную информацию, и отправлять на сервера Qualcomm. Найти данный раздел можно по этой ссылке выбрав в правом меню Getting Started SDK -> Step 3: Compiling & Running… -> Publish Your Application. И плюс к этому, можете считать меня параноиком, но я на 90% уверен, что когда их SDK наберет определенный процент популярности, они скажут “Все, халява закончилась, платите бабки”.

Поэтому, считаю разработку собственного движка не пустой тратой времени.

Собственно, к делу!

Теория


Считаем, что к этому моменту вы внедрили OpenCV в ваш проект (как?), и уже написали метод распознавания объекта в кадре поступающем с камеры. То есть, примерно такая картинка у вас есть:

image

Теорию по данному вопросу можно найти во многих источниках. Основные ссылки я привел внизу. Стартовым ресурсом является страничка документации OpenCV, хотя вопросов по прочтению останется много.

В двух словах, для построения 3D пространства по найденой 2D гомографии, нам нужно знать 2 матрицы:
  • Внутрення матрица (intrinsic matrix), или матрица камеры – эта матрица состоит из параметров камеры – фокальное расстояние по двум осям (fx, fy) и координата центра фокуса (cx, cy).
    Структура данной матрицы:
    image
  • Внешняя матрица (extrinsic matrix), или матрица модели – это матрица растяжения, поворота и переноса модели. Она, собственно, однозначно задает положение объекта в пространстве. Структура:
    image,
    где диагональные элементы отвечают за растяжение, остальные элементы r за поворот, и элементы t – за перенос.
    Примечание: в целом, структура данной матрицы может варьироваться в зависимости от модели (уравнений преобразований координат). Например, в тот же OpenGL, передается матрица немного другой структуры, но ключевые элементы в ней всегда присутствуют.

Практика


На практике, для того чтобы OpenGL отрендерил 3D модельку поверх нашего объекта, нам нужно ему задать:
  • Проекционную матрицу — GL_PROJECTION.
  • Модельную матрицу — GL_MODELVIEW.

Примечание: В iOS вы можете использовать 2 версии OpenGLES – 1.1 и 2.0. Основное отличие – наличие шейдеров во второй версии. В обоих случаях, мы должны задать 2 матрицы, только в первом случае они задаются конструкцией типа:

glMatrixMode(GL_PROJECTION);
glLoadMatrixf(projectionMatrix);
        
glMatrixMode(GL_MODELVIEW);
glLoadMatrixf(modelViewMatrix);


А во втором, вы передаете их на вход шейдерам.

Далее, обусловимся что размер кадра, который вы получаете с камеры cameraSize = (width, height). В моем случае cameraSize=(640, 480).

Разберемся как строить каждую матрицу.

Проекционная матрица

Данная матрица строится на основе матрицы камеры. Как было показано выше, последняя состоит из определенных параметров камеры. В теории, можно высчитать эти параметры основываясь на технических характеристиках камеры, но на практике этого никто не делает.

Процесс нахождения параметров камеры называется ее калибрацией. В OpenCV написаны все необходимые функции для выполнения калибрации. Также, есть пример, который позволяет просто в «онлайне» откалибровать свою веб-камеру. Но нам же нужно откалибровать камеру на устройстве – iPhone/iPad/iPod. И здесь я пошел по пути описанном тут.

Калибровать камеру мы будем «оффлайн». Это означает, что мы сделаем снимки калибровачного шаблона (шахматной доски) камерой нашего устройства, перенесем фотографии на компьютер, и посчитаем параметры с этих фоток. Несколько моментов:
  • Шаблон шахматной доски распечатайте на лист формата A4. Лист должен быть чистым, очень желательно чтобы принтер «не тек».
  • Постарайтесь чтобы на снимках лист шахматной доски лежал ровно на поверхности – края были не загнуты, какие-либо изгибы листа отсутствовали. Для этого, можно приклеить его к картонке, либо просто положить на края тяжелые предметы, но чтобы они не закрывали вид шаблона.
  • Снимки с камеры нужно перевести в размер cameraSize. Это не совсем очевидный шаг – например на iPhone 4, при распознавании я использую размер кадра камеры 640x480, а когда фоткаешь простой камерой и скидываешь на компьютер – получаешь фотки большего размера (2592x1936), я их сжимал до 640x480, и эти использовал в программе.
  • Количество снимков шаблона должно быть 12-16 + сделаны с разных ракурсов. Лично я использовал 16 снимков, при этом изменение параметров построенных на 12 и на 16 хоть и не большое, но присутствует. Вообще, чем больше снимков, тем параметры точнее, а эта точность далее будет влиять на наличие/отсутствие сдвигов при построении 3D-объектов.
  • Помимо матрицы камеры, на данном этапе мы находим коэффициенты искажения (distortion coefficients). В двух словах, эти коэффициенты описывают искажение картинки линзой камеры. Более подробно можно почитать в документации OpenCV и в Википедии. Что касается мобильных устройств, то у большинства камеры достаточно качественные, и эти коэффициенты сравнительно малы, поэтому их можно не учитывать.

Xcode проект программы для калибрации вы можете скачать тут. При этом у вас должен быть скомпилирован OpenCV. Либо же вы можете скачать скомпилированный фреймворк отсюда.
Сами фотографии, нужно положить в папку со скомпилированным бинарником, и переименовать по порядку.
Если все будет ок, вы получите 2 файла на выходе программы:
  • Intrinsics.xml – тут будет построчно записана матрица камеры 3x3
  • Distortion.xml – тут будут посчитанные коэффициенты.

Если на каких-то снимках шаблон не будет найден – попробуйте заменить эти снимки на другие, с лучшим освещением, возможно под менее острым углом к шаблону. OpenCV должен легко находить все внутренние точки шаблона.
Имея числа с данных файлов, мы можем построить матрицу проекции для OpenGL.

float cameraMatrix[9] = {6.24860291e+02, 0., cameraSize.width*0.5f, 0., 6.24860291e+02, cameraSize.height*0.5f, 0., 0., 1.};

- (void)buildProjectionMatrix {    
    // Camera parameters
    double f_x = cameraMatrix[0]; // Focal length in x axis
    double f_y = cameraMatrix[4]; // Focal length in y axis (usually the same?)
    double c_x = cameraMatrix[2]; // Camera primary point x
    double c_y = cameraMatrix[5]; // Camera primary point y
    
    double screen_width = cameraSize.width; // In pixels
    double screen_height = cameraSize.height; // In pixels
    
    double near = 0.1;  // Near clipping distance
    double far = 1000;  // Far clipping distance
    
    projectionMatrix[0] = 2.0 * f_x / screen_width;
	projectionMatrix[1] = 0.0;
	projectionMatrix[2] = 0.0;
	projectionMatrix[3] = 0.0;
    
	projectionMatrix[4] = 0.0;
	projectionMatrix[5] = 2.0 * f_y / screen_height;
	projectionMatrix[6] = 0.0;
	projectionMatrix[7] = 0.0;
	
	projectionMatrix[8] = 2.0 * c_x / screen_width - 1.0;
	projectionMatrix[9] = 2.0 * c_y / screen_height - 1.0;	
	projectionMatrix[10] = -( far+near ) / ( far - near );
	projectionMatrix[11] = -1.0;
    
	projectionMatrix[12] = 0.0;
	projectionMatrix[13] = 0.0;
	projectionMatrix[14] = -2.0 * far * near / ( far - near );		
	projectionMatrix[15] = 0.0;
}


Несколько замечаний:
  • Коэффициенты (cx, cy) матрицы камеры мы заменяем на центр нашего кадра. Тогда смещения 3D модели относительно объекта в кадре не будет. К такому же выводу пришел и автор поста (см. UPDATE: в конце статьи).
  • Сами формулы получения матрицы проекции я взял отсюда. По сути, таким образом задается перспективная проекция, которая учитывает параметры нашей камеры и размер кадра.
  • Представленная матрица камеры была получена мною для iPhone 4. Для других устройств матрица будет отличаться, хотя, я думаю, не намного.


Матрица модели

При построении данной матрицы мне помог вот этот вопрос в StackOverflow.
Благо, в OpenCV необходимые функции уже рализованы.

Итак, код:
float cameraMatrix[9] = {6.24860291e+02, 0., cameraSize.width*0.5f, 0., 6.24860291e+02, cameraSize.height*0.5f, 0., 0., 1.};
float distCoeff[5] = {1.61426172e-01, -5.95113218e-01, 7.10574386e-04, -1.91498715e-02, 1.66041708e+00};

- (void)buildModelViewMatrixUseOld:(BOOL)useOld {
    clock_t timer;
    startTimer(&timer);
    
    CvMat cvCameraMatrix = cvMat( 3, 3, CV_32FC1, (void*)cameraMatrix );
	  CvMat cvDistortionMatrix = cvMat( 1, 5, CV_32FC1, (void*)distCoeff );
    CvMat* objectPoints = cvCreateMat( 4, 3, CV_32FC1 );
    CvMat* imagePoints = cvCreateMat( 4, 2, CV_32FC1 );
    
//  Defining object points and image points
    int minDimension = MIN(detector->modelWidth, detector->modelHeight)*0.5f;
    for (int i=0; i<4; i++) {
        float objectX = (detector->x_corner[i] - detector->modelWidth/2.0f)/minDimension;
        float objectY = (detector->y_corner[i] - detector->modelHeight/2.0f)/minDimension;
        
        cvmSet(objectPoints, i, 0, objectX);
        cvmSet(objectPoints, i, 1, objectY);
        cvmSet(objectPoints, i, 2, 0.0f);
        cvmSet(imagePoints, i, 0, detector->detected_x_corner[i]);
        cvmSet(imagePoints, i, 1, detector->detected_y_corner[i]);
    }

    CvMat* rvec = cvCreateMat(1, 3, CV_32FC1);
    CvMat* tvec = cvCreateMat(1, 3, CV_32FC1);
    CvMat* rotMat = cvCreateMat(3, 3, CV_32FC1);
    
    cvFindExtrinsicCameraParams2(objectPoints, imagePoints, &cvCameraMatrix, &cvDistortionMatrix,
                                 rvec, tvec);
    
//    Convert it 
    CV_MAT_ELEM(*rvec, float, 0, 1) *= -1.0;
    CV_MAT_ELEM(*rvec, float, 0, 2) *= -1.0;
    
    cvRodrigues2(rvec, rotMat);
    
    GLfloat RTMat[16] = {cvmGet(rotMat, 0, 0), cvmGet(rotMat, 1, 0), cvmGet(rotMat, 2, 0), 0.0f,
                        cvmGet(rotMat, 0, 1), cvmGet(rotMat, 1, 1), cvmGet(rotMat, 2, 1), 0.0f,
                        cvmGet(rotMat, 0, 2), cvmGet(rotMat, 1, 2), cvmGet(rotMat, 2, 2), 0.0f,
                        cvmGet(tvec, 0, 0)  , -cvmGet(tvec, 0, 1), -cvmGet(tvec, 0, 2),    1.0f};
        
    cvReleaseMat(&objectPoints);
    cvReleaseMat(&imagePoints);
    cvReleaseMat(&rvec);
    cvReleaseMat(&tvec);
    cvReleaseMat(&rotMat);
    
    printTimerWithPrefix((char*)"ModelView matrix computation", timer);
}

Для начала нам нужно определить 4 пары точек объекта и соответствующего положения в кадре.
Точки положения в кадре – это вершины четырехугольника, описывающего (ограничивающего) объект в кадре. Получить данные точки, имея гомографию преобразования H, можно просто подействовав на крайние точки шаблона данной гомографией:

image

Относительно точек самого объекта есть пару моментов:
  • Точки объекта задаются в 3D, а точки на кадре в 2D. Соответственно, если допустим мы зададим им не нулевое значение z, то начало координат на z будет сдвинуто относительно плоскости объекта на кадре. Проще понять из данных двух картинок:

    image
    z = 1.0

    image
    z = 0.0

  • Также, точки объекта мы задаем так, чтобы дальше нам было удобно работать в данном 3D пространстве. Например, я хочу чтобы начало координат было прямо по центру шаблона, а единица длины равнялась половине меньшей стороны (в коде minDimension). В этом случае, мы не будем зависеть от конкретных размеров шаблона в пикселях, а 3D пространство будет отмасштабировано по меньшей стороне.

Сконструированные матрицы передаются функции cvFindExtrinsicCameraParams2. Она построит нам вектор поворота, и вектор переноса. Из вектора поворота нам нужно получить матрицу поворота. Это делается с помощью функции cvRodrigues2, предварительно немного преобразовав вектор поворота умножением второго и третьего элементов на -1. Далее, нам остается только сохранить полученные данные в модельную матрицу для OpenGL. При этом, матрица OpenGL должна быть транспонирована.
Все, удаляем временные объекты, и матрица модели получена.

Итого


Имея процедуру построения двух матриц мы можем смело создавать GLView, и рисовать там модельки. Замечу, что функция нахождения матрицы модели выполняется не более 10 миллисекунд на iPhone 4, то есть ее использование не понизит FPS вашего распознавания значительно.
Спасибо за внимание.

Learn more:


1. http://old.uvr.gist.ac.kr/wlee/web/techReports/ar/Camera%20Models.html
2. http://www.hitl.washington.edu/artoolkit/mail-archive/message-thread-00653-Re--Questions-concering-.html
3. http://sightations.wordpress.com/2010/08/03/simulating-calibrated-cameras-in-opengl/
4. http://www.songho.ca/opengl/gl_projectionmatrix.html
Tags:
Hubs:
Total votes 59: ↑59 and ↓0 +59
Views 15K
Comments Comments 9