Kinect: избавляемся от искажений при помощи потолка

Идеально плоский потолокДавеча я писал про то, как выглядят данные, которые приходят с сенсора. Такое поведение сенсора просто возмутительно и долго это продолжаться не может.

Ок. А что нам потребуется? Нам потребуется потолок. Я внимательно его осмотрел и убедился, что в реальности он плоский. А вот на восстановленом изображении — нифига.

Для начала нам нужно сохранить дамп глубин «идеального» потолка. Можно и стену если у вас есть достаточно плоской поверхности. :)

В файл будет сохранена двумерная матрица из чисел разделенных пробелами. Строки по строкам и т.д. Позже поймем зачем именно так.

Кусочек файла

888 889 890 890 890 890 890 891 890 891 891 892 892 892 892 892 892 892 892 892 891 893 892 892 892 892 892 ... 2047 2047 2047 2047 2047 2047
...

Теперь посмотрим на картинку этой самой плоскости. Для этого запускаем maxima.

(%i1) load(numericalio);
(%i2) m: read_matrix("<путь к файлу с глубинами>");
($i3) f(x, y) := float('m [round(x), round(y)]); 
-- тут мы зададим функцию извлечения нужного значения из матрицы. 
-- Так как матрица наши данные дискретны 
-- и существуют только в определенных точках
(%i4) plot3d (f(x, y), [x, 1, 640], [y, 1, 480]);

О! Круто. Мы что-то видим.

Потолок. Необработанная карта глубин

Что-то не то :)

Стоит заметить, что в режиме сырых данных FREENECT_DEPTH_11BIT самые правые 8 столбцов не используются и всегда возвращают значение FREENECT_DEPTH_RAW_MAX_VALUE. Об этом не стоит забывать при последующих действиях.

Поэтому нам нужно посмотреть плоскость без правых столбцов (8 штук).

(%i4) plot3d (f(x, y), [x, 1, 632], [y, 1, 480]);

И тут уже то, что нам надо.

Потолок: карта глубин после доработкиЭто плоскость. Но не очень плоская. И таки да. Надо сделать ее обратно плоской. :)

Мы знаем, что все точки данной поверхности равноудалены от плоскости, где расположен сенсор.

Поэтому можно ввести коэффициент нормировки для каждой из точек (x, y). Который будет равен отношению реального расстояния к полученному.

Зная этот коэффициент можно узнать, реальное расстояние до точки. Просто умножив полученное от кинекта значение на коэффициент нормировки в данной точке (x, y).

Коэффициенты считаем один раз по калибровочному изображению. А затем загружаем в свое приложение и пользуемся.

Минусы решения

  • Я предположил, что коэффициент нормировки зависит только от координат (x, y), но не зависит от глубины. Так это или нет — проверю. (пока датчик говорит, что я прав)
  • Плоскость по которой выполняется калибровка должна находится на расстоянии 2,5-3,5 метров от сенсора. Именно при таком положении коэффициенты нормировки будет точными.
  • Так как сенсор довольно чувствителен, то его положение должно быть параллельно плоскости калибровки (мне помогал обычный строительный уровень)
  • Так же перед калибровкой полученное изображение плоскости нужно сгладить для того, чтобы избавиться от шумов (это умеет делать матлабоский smoothn)

UPD

Код на гитхабе

Kinect: о восстановлении координат и абберациях разного рода

Вы не подумайте ничего - это плоскость.Нет. Вы не подцмайте ничего. это плоскость — вид сбоку. А точнее мой потолок. И не посмотри я на него через сенсоры кинета, то в жизни бы не узнал, насколько он «плоский». :-D На самом деле потолок-то плоский. Но только разного рода нелинейные искажения, которые вносят сенсор и линзы не компенсируются ни встроенными калибровочными константами (которые зашиваются в каждую модель на заводе), ни функцией преобразования глубины кинекта в глубину реальную. Т.е. в метры/миллиметры.

Так что же не так?

Начнем с самого начала. Функция преобразования данных с датчика в глубину нелинейна по своей природе. Т.е. чем ближе объект наблюдения, чем с большей точностью мы можем мерять расстояние до объекта. Чем объект дальше, тем точность меньше. А это значит, что на 1 диницу шага датчика на ближнем расстоянии приходится меньше миллиметров.

На сейчас ребята из openni предлагают новую версию функции преобразования сырых данных в метрические.

0.1236 * tan(rawDisparity / 2842.5 + 1.1863)

Эта формула даст нам значения в метрах. Есть еще одна формула (она предлагалась раньше).

1/(rawDisparity * -0.0030711016 + 3.3309495161)

Теперь говорят, что она морально устарела. :)

Посмотрим на обе формулы.

Функция вычисления реальной глубины изображенияТут мы видим, что обе дают практически одинаковый результат. Даже если их и увеличить, то расхождения будут заметны только на сильно высоких значениях датчика. Стоит заметить, что по иксу — это сырые данные. А по игреку — восстановленное расстояние (в миллиметрах. для удобства обе функции были домножены на 1000).

Ок. Расстояние есть. Дальше нужно получать как-то координаты x и y. Так как с одной глубиной ничего интересного не получиться.

Тут все интереснее.

Мы знаем из спецификации сенсора (я говорю про первую версию если что):

  • угол обзора по горизонтали — 58 градусов
  • угол обзора по вертикали — 45 градусов
  • количество точек с датчика — 640х480

И тут у нас должно появиться подозрение на эти спеки. Почему? Потому что ничего не говорится про относительный шаг между точками по горизонтали и по вертикали.

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

Но что же получается. А получается следующее (изображение уперто отсюда).

Поиск расстояния

tan(45/2)/tan(58/2) = 0,747261047

А это значит, что точка по вертикали будет 0,747261047, а точка по горизонтали — 1.

Отлично. соотношение расстояний между точками по горизонтали и вертикали нашли.

Тперь надо найти точки.

После того, как данные глубины получены с кинекта формируем массив координат.

(x_v, y_v. raw_depth)

Здесь x_v и y_v — это всего лишь индексы строки и столба в двумерном массиве глубин.

x_v = [0, 639]
y_v = [0, 479]

дальше надо лишь превратить эту точку в точку с реальными координатами.

z_w = 123.6 * tan(depth / 2842.5 + 1.1863)

Эм. А x_w и y_w? Тут все просто. По картинке выше мы знаем, что

x_v/f = x_w/z_w

Где f — это расстояние от камеры, то вьюпорта.

f = MAX_X/2 / tan(58/2) = 639/2 / tan(58/2)  = 319.5/tan(58/2)

Или аналогично для оси ординат. Но тут не стоит забывать про коэффициент отношения между расстоянием по горизонтали и вертикали.

f = 0.747261047*239.5/tan(45/2)

Теперь все просто

x_w = (x_v - 640/2) * z_w / f
y_w = (y_v - 480/2) * 0.747261047 * z_w / f

В случае с y_w не забываем про масштабный коэффициент.

Но все эти вфводы позволяют нам лиш частично восстановить изображение. И все потому, что функция глубины — это фукция от трех переменных: x_v, y_v и depth, а не просто от depth. :) Так как влияние разного рода аббераций слишком велико (не то чтобы слишком +-10 самнтиметров по краям на растоянии 2 метра).

Поэтому пока мой потолок будет зображать из себя часть поверхности сферы (или цилиндра) неопределенного радиуса.

Потолок (или часть сферы?)

UPD

Код на гитхабе

Google JS API для AngularJS

Написал небольшой враппер к гугловому апи для использования совместно с ангуляром. Взять можно на гитхабе.

Или в bower

 $ bower install angularjs-gapi

Поключение

angular.module('app', ['gapi']).config(['gapiProvider', '$routeProvider', function(gapiProvider, $routeProvider) {
   gapiProvider.apiKey(YOU_API_KEY) // апи-ключ можно создать в консоли разработчика
        .clientId(YOU_APP_CLIENT_ID) // берем в консоли разработчика
        .apiScope(SCOPES_FOR_APP); // скоупы, которые нужны для работы приложения
}])

Если вам не требуется работать с пользовательскими данными и не требуется разрешение пользователя, то достаточно использования только YOU_API_KEY.

Авторизация

angular.module('app').controller('tstController', ['$scope', 'gapi', function($scope, gapi) {
  gapi.login().then(function() {
    $scope.login = 'success';
  }, function() {
    $scope.login = 'fail';
  });
}])

Выполнение запросов не требующих авторизации

angular.module('app').controller('tstController', ['$scope', 'gapi', function($scope, gapi) {
  // we can't make requests while api is not ready
  if (gapi.isApiReady()) {
    gapi.call("youtube", "v3", "search", "list", {
      query: "search term"
      part: "snippet"
      type: "video"
    }).then(function(response) {
      // work with response
    })
  }
}]);

Выполнение запросов требующих авторизации

angular.module('app').controller('tstController', ['$scope', 'gapi', function($scope, gapi) {
  // we can't make requests while api is not ready and user is not logged in
  if (gapi.isApiReady() && gapi.isLoggedIn()) {
      gapi.call("youtube", "v3", "playlists", "list", {
      part: "snippet",
      type: "video";
    }).then(function(response) {
      // work with response
    })
  }
}]);

Вроде все.

Grunt для самых маленьких

На проекте есть папка с с js и стилями css.

Нужно все это минифицировать.

1 — нам нужен грант (установленный локально).

Предполагается, что глобально грант уже установлен.

Если нет, то

 $ sudo yum install nodejs-grunt*

Ставим нужное

 $ npm install grunt
$ npm-install grunt-contrib-unglify grunt-contrib-watch grunt-contrib-cssmin grunt-contrib-concat

Если не поставить модули локально, то получим ошибку

Unable to find local grunt

2 — создаем файл с описанием проекта package.json

{
  "name": "<project name>",
  "version": "0.1.0",
  "devDependencies": {
    "grunt": "~0.4.5",
    "grunt-contrib-concat": "^0.4.0",
    "grunt-contrib-cssmin": "^0.10.0",
    "grunt-contrib-uglify": "^0.5.0",
    "grunt-contrib-watch": "*"
  },
  "dependencies": {
    "grunt": "^0.4.5",
    "grunt-ts": "^1.11.13"
  }
}

3 — создаем сценарий для работы

module.exports = function (grunt) {
    // 1 - Описываем все выполняемые задачи
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        concat: {
            css: {
                src: ['src/**/*.css'],
                dest: 'dist/app.css'
            },
            js: {
                src: ['src/js/**/*.js'],
                dest: 'dist/app.js'
            }
        },
        cssmin: {
            css: {
                src: 'dist/app.css',
                dest: 'dist/app.min.css'
            }
        },
        uglify: {
            js: {
                src: 'dist/app.js',
                dest: 'dist/app.min.js'
            }
        },
        watch: {
            css: {
                files: ['src/css/**/*.css'],
                tasks: ['concat:css', 'cssmin:css']
            },
            js: {
                files: ['src/js/**/*.js'],
                tasks: ['concat:js', 'uglify:js']
            }
        }
    });

    // 2 - Загружаем нужные плагины
    grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.loadNpmTasks('grunt-contrib-cssmin');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-watch');

    // 3 - Говорим grunt, что мы хотим сделать, когда напечатаем grunt в терминале.
    grunt.registerTask('default', ['concat', 'cssmin', 'uglify']);

};

4 — печатаем grunt в терминали

$ grunt
Running "concat:js" (concat) task
File gapi.js created.

Running "uglify:js" (uglify) task
>> 1 file created.

Done, without errors.

Kinect: Приведение координат сенсора в метрические

В одниокм из своих проектов с использованием сенсора пришлось задаваться вопросом: как привиодить координаты сенсора (глубину) в координаты метрические. Если этого не сделать, то прямые углы у стен прямыми не будут.

Будет это выглядеть как на картинке ниже.

Kinect: сырой рендер (без преобразования в метрические координаты)А как это сделать?

В пакете freenect есть демка glpclview в коде которой можно увидеть матрицу преобразований координат сенсора в нужные нам координаты.

// Do the projection from u,v,depth to X,Y,Z directly in an opengl matrix
// These numbers come from a combination of the ros kinect_node wiki, and
// nicolas burrus' posts.
void LoadVertexMatrix()
{
  float fx = 594.21f;
  float fy = 591.04f;
  float a = -0.0030711f;
  float b = 3.3309495f;
  float cx = 339.5f;
  float cy = 242.7f;
  GLfloat mat[16] = {
    1/fx,     0,  0, 0,
    0,    -1/fy,  0, 0,
    0,       0,  0, a,
    -cx/fx, cy/fy, -1, b
  };
  glMultMatrixf(mat);
}

Не будем заострять внимание на очень понятных комментариях в коде :), а попробуем понять, чтоже эта штука делает.

Сходу информации почти нет — гугл говорит, что это есть лишь приведение координат согласно калибровочным данным самого кинекта (тыц).

Но это нифига не проясняет.

Дальнейшее гугление нашло пруф на форуме ROS, а так же пруф в гуглогруппах.

В итоге на скорую руку был состряпан код, который делает нужное преобразование.

/**
 * Преобразует глубину в реальное значение (в миллиметрах)
 */
double raw_depth_to_millimeters(int depth_value){
  double depth_value_f = (float) depth_value;
  if (depth_value < 2047){
    float depth = 1000.0 / (depth_value_f  * -0.0030711016 + 3.3309495161);
    return depth;
  }
  return 0.0f;
}

/**
 * Преобразует вируальную точку point в точку с реальными координатами (в миллиметрах)
 */
Ogre::Vector3 depth_to_realword(Ogre::Vector3 point){
  double fx_d = 1.0 / 5.9421434211923247e+02;
  double fy_d = 1.0 / 5.9104053696870778e+02;
  double cx_d = 3.3930780975300314e+02;
  double cy_d = 2.4273913761751615e+02;

  double depth = raw_depth_to_millimeters(point.z);

  return Ogre::Vector3(
    (point.x - cx_d) * depth * fx_d,
    (point.y - cy_d) * depth * fy_d,
    depth);
}

Про этот код важно помнить, что координаты x и y должны назодиться в первой четверти (больше нуля).

Теперь изображение выглядит куда лучше. :)

Изображение с сенсора kineck после преобразования в метрические координаты.

Ogre3D: работа с вертексным буфером

Ура :) Я научился таки работать с вертексным буфером в этом самам огре.

Как было просто в голом опенглы. Так просто, что даже вспомнить не хочется. :-D

Теперь же нам надо сделать что-то вроде этого

void TutorialApplication::createScene(void)
{

    /* Зачем дополнительно создавать submesh пока не понял */
    Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().createManual("CustomMesh", "General");
    Ogre::SubMesh *subMesh = mesh->createSubMesh();

    /* Подготавливаем структуру для трех вершин (треугольник у нас) */
    mesh->sharedVertexData = new Ogre::VertexData;
    mesh->sharedVertexData->vertexCount = 3;

    /* Получаем ссылку на дескриптор буфера (описывает структуру) */
    Ogre::VertexDeclaration *decl = mesh->sharedVertexData->vertexDeclaration;
    size_t offset = 0;

    /* первый элемент буфера - это сама вершина (ее координаты) */
    decl->addElement(0, offset, Ogre::VET_FLOAT3, Ogre::VES_POSITION);
    offset += Ogre::VertexElement::getTypeSize(Ogre::VET_FLOAT3);

    /* вторая часть буфера - нормаль вершины */
    decl->addElement(0, offset, Ogre::VET_FLOAT3, Ogre::VES_NORMAL);
    offset += Ogre::VertexElement::getTypeSize(Ogre::VET_FLOAT3);

    /* Третья часть - это цвет вершины */
    decl->addElement(0, offset, Ogre::VET_COLOUR, Ogre::VES_DIFFUSE);
    offset += Ogre::VertexElement::getTypeSize(Ogre::VET_COLOUR);

    /* Генерируем вертексный буфер по описанию, которое выше */
    Ogre::HardwareVertexBufferSharedPtr vertexBuffer = Ogre::HardwareBufferManager::getSingleton().
        createVertexBuffer(offset, mesh->sharedVertexData->vertexCount, Ogre::HardwareBuffer::HBU_STATIC);

    // подготавливаем цвета
    // можно писать цвета руками и использовать не Ogre::VET_COLOUR, а VET_FLOAT3|4 (4 - это если альфаканал нужен)
    Ogre::RenderSystem* rs = Ogre::Root::getSingleton().getRenderSystem();
    Ogre::uint32 red, green, blue;
    rs->convertColourValue(Ogre::ColourValue(1,0,0,1), &red);
    rs->convertColourValue(Ogre::ColourValue(0,1,0,1), &green);
    rs->convertColourValue(Ogre::ColourValue(0,0,1,1), &blue);

    const float sqrt13 = 0.577350269f; /* sqrt(1/3) - это для нормалей */

    /* блокируем буфер на запись и берем указатель на него */
    float *pVertex = static_cast<float *>(vertexBuffer->lock(Ogre::HardwareBuffer::HBL_DISCARD));

    // Заполняем буфер
    *pVertex++ = 0.0f; *pVertex++ = 1.0f; *pVertex++ = 0.0f; // вершина
    *pVertex++ = -sqrt13; *pVertex++ = sqrt13; *pVertex++ = -sqrt13; // нормаль
    *(*(Ogre::uint32**)&pVertex)++ = red;   //цвета
    *pVertex++ = -1.0f; *pVertex++ = -1.0f; *pVertex++ = 0.0f; // вершина
    *pVertex++ = sqrt13; *pVertex++ = sqrt13; *pVertex++ = -sqrt13; // нормаль
    *(*(Ogre::uint32**)&pVertex)++ = green; // цвета
    *pVertex++ = 1.0f; *pVertex++ = -1.0f; *pVertex++ = 0.0f; // вершина
    *pVertex++ = -sqrt13; *pVertex++ = -sqrt13; *pVertex++ = -sqrt13; // нормаль
    *(*(Ogre::uint32**)&pVertex)++ = blue;  // цвета

    /* разблокируем */
    vertexBuffer->unlock();

    /* Создаем буфер для индексов */
    Ogre::HardwareIndexBufferSharedPtr indexBuffer = Ogre::HardwareBufferManager::getSingleton().
        createIndexBuffer(Ogre::HardwareIndexBuffer::IT_16BIT, mesh->sharedVertexData->vertexCount, Ogre::HardwareBuffer::HBU_STATIC);

    /* Получаем блокировку на запись и пишем индексы в буфер */
    uint16_t *indices = static_cast<uint16_t *>(indexBuffer->lock(Ogre::HardwareBuffer::HBL_NORMAL));

    /* Задаем нужный индексы вершин, которые будет треугольник представлять */
    indices[0] = 0;
    indices[1] = 1;
    indices[2] = 2;

    /* записали - разблокировали */
    indexBuffer->unlock();

    /* Теперь надо прицепить к нашей геометрии созданный буфер */
    mesh->sharedVertexData->vertexBufferBinding->setBinding(0, vertexBuffer);
    subMesh->useSharedVertices = true;
    subMesh->indexData->indexBuffer = indexBuffer;
    subMesh->indexData->indexCount = mesh->sharedVertexData->vertexCount;
    subMesh->indexData->indexStart = 0;

    /* Если не объявить рамку, то огр не сможет правильно обсчитать сетку
     * и она будет видна лишь в корневой ноде (если ее туда прицепить),
     * а в дочерних - не будет.
     * Для этого можно зачитать http://www.ogre3d.org/forums/viewtopic.php?f=2&t=60200
     */
    mesh->_setBounds(Ogre::AxisAlignedBox(-1, -1, -1, 1, 1, 1));

    /* нарисовали - грузим */
    mesh->load();

    /*
     * А теперь нужно задефайнить материал.
     * Если этого не сделать, то новоиспеченный триангл будет выглядеть белым,
     * а не многоцветным как задумано выше
     */
    Ogre::MaterialPtr material = Ogre::MaterialManager::getSingleton().create("Test/ColourTest", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
    material->getTechnique(0)->getPass(0)->setVertexColourTracking(Ogre::TVC_AMBIENT);

    /* Создаем ноду на базе того, что накодили выше. */
    Ogre::Entity *entity = mSceneMgr->createEntity("CustomEntity", "CustomMesh", "General");
    entity->setMaterialName("Test/ColourTest", "General");
    Ogre::SceneNode *node = mSceneMgr->getRootSceneNode()->createChildSceneNode();
    node->attachObject(entity);

    mCamera->lookAt(Ogre::Vector3(0, 0, 0));
    mCamera->setPosition(Ogre::Vector3(0, 10, 50));
}

 

Ogre3D - использование вертексного буфераЧего почитать:

 

 

Запускаем Dwarf Fortress на Fedora, Ubuntu и др. x64

У последнего билда есть несколько бед:

Но сначала надо поставить 32х битные версии нужных либ

$ sudo yum install SLD.i686 SDL_image.i686 openal-soft.i686 SDL_tff.i686

Оно может попросить что-то еще, но что — не помню (у меня до этого было все установлено :)).

Первая беда — это

Not found: data/art/curses_640x300.png

Эта беда лечится запуском df в виде

$ LD_PRELOAD=/usr/lib/libz.so.1 ./df

Вторая — это

Dynamically loading the OpenAL library failed, disabling sound

Лечим

$ sudo ln -s /usr/lib/libopenal.so.1 /usr/lib/libopenal.so
$ sudo ln -s /usr/lib/libsndfile.so.1 /usr/lib/libsndfile.so
$ sudo ldconfig

Рубимся :)

Dwart FortressСсылки