OpenNI 2 的座標系統轉換

在之前的《OpenNI 2 VideoStream 與 Device 的設定與使用》一文裡,已經有解釋怎麼樣用 OpenNI 2 來讀取感應器的彩色以及深度影像了。接下來,就是 OpenNI 的座標系統的問題了~

首先,在 OpenNI 1.x 的座標系統,基本上分成「投影座標系統」(Projective Coordinate System)「真實世界座標系統」(Real World Coordinate System)兩種,這在之前的《OpenNI 的座標系統》已經有介紹過了,如果還沒看過的,請稍微閱讀一下這篇文章。

到了 OpenNI 2,在這部分的概念,在名詞的部分,則是稍微做了一些修改。座標系統變成是以「深度座標系統」(Depth)「世界座標系統」(World)這兩者為主;不過,雖然名字換了,但是實際上「深度座標系統」就是以前的「投影座標系統」,而「世界座標系統」就是以前的「真實世界座標系統」。

而在座標系統的轉換 API 的部分,OpenNI 2 則是提供了名為 openni::CoordinateConverter官網)的類別,專門用來做座標系統的轉換。它總共提供了五個 static 函式、三種轉換的方向,包括了:

  • 世界座標系統到深度座標系統
    Status convertWorldToDepth( const VideoStream& depthStream,
    float worldX, float worldY, float worldZ,
    int* pDepthX, int* pDepthY, DepthPixel* pDepthZ)
  • 深度座標系統到世界座標系統
    Status convertDepthToWorld( const VideoStream& depthStream,
    int depthX, int depthY, DepthPixel depthZ,
    float* pWorldX, float* pWorldY, float* pWorldZ)
  • 深度座標系統到彩色影像
    Status convertDepthToColor( const VideoStream& depthStream,
    const VideoStream& colorStream,
    int depthX, int depthY, DepthPixel depthZ,
    int* pColorX, int* pColorY)

前兩者基本上就是把單一個點,在「深度座標系統」與「世界座標系統」間進行轉換。而根據世界座標系統的值型別的不同(float 的版本),這兩個函式都各還有另一個版本,不過用法完全相同。而這兩種函式,基本上就等同於 OpenNI 1.x Depth Generator 提供 ConvertProjectiveToRealWorld()ConvertRealWorldToProjective() 這兩個函式,比較不一樣的是,OpenNI 1.x 所提供的介面,是可以大量進行轉換的,而 OpenNI 2 的介面,一次只能轉換一個點。

而實際在使用的時候,大致上就會像這樣:

VideoFrameRef vfDepthFrame;
const DepthPixel* pDepthArray = NULL;
if( g_vsDepthStream.readFrame( &vfDepthFrame ) == STATUS_OK )
{
pDepthArray = (const DepthPixel*)vfDepthFrame.getData();
for( int y = 0; y < vfDepthFrame.getHeight() - 1; y )
{
for( int x = 0; x < vfDepthFrame.getWidth() - 1; x )
{
int idx = x y * vfDepthFrame.getWidth();
const DepthPixel& rDepth = pDepthArray[idx];
float fX, fY, fZ;
CoordinateConverter::convertDepthToWorld( g_vsDepthStream,
x, y, rDepth,
&fX, &fY, &fZ );

}
}
}

在上面的例子裡,g_vsDepthStream 就是一個深度感應器的 VideoStream,而當取得他的資料之後,接下來就是透過兩層迴圈,依序地去讀取出深度影像裡面的每一個點的值;而這時候,這個點在深度影像中的位置 ( x, y ),再加上該點的深度值 rDepth,所構成的 ( x, y, rDepth ) 就是一個位在深度座標系統內的一個點。

而要把這個點轉換成為世界座標系統裡的位置的話,就是要呼叫 CoordinateConverter 提供的 convertDepthToWorld() 這個函式,也就是上方黃底的部分。而呼叫這個函式時,需要的參數包含了三個部分:

  • 儲存轉換資訊的深度感應器的 VideoStreamvsDepthStream
  • 輸入的資料、也就深度座標系統的點位:xyrDepth 
  • 輸出的結果,也就世界座標系統的點位:fXfYfZ

上面的程式執行完之後,( fX, fY, fZ ) 就是深度影像中 ( x, y ) 這一點,在世界座標系統內的位置了~基本上,在轉換前後,兩個座標系統內的 Z 的值基本上是不會變的,也就是 rDepth 的值會等於 fZ

而如果把這些點在 3D 環境(OpenGL 或 Direct 3D)裡依序畫出來,基本上就會變成像是右邊的樣子,或者也可以參考之前版本的影片(YouTube)。

完整的 OpenGL 範例程式,Heresy 之後s之後會再補上來。

convertWorldToDepth() 則就是提供一個反向的轉換,可以把空間中的點,計算出他在深度影像上會是在哪個一位置、值是多少,基本上也是同樣的使用方法。


另外和 OpenNI 1.x 比起來比較特別的是,OpenNI 2 還有提供一個 convertDepthToColor() 的函式,可以把深度影像上的點,轉換到彩色影像上。根據官方文件(連結)的說法,他的轉換結果,和透過 Device 提供的 setImageRegistrationMode() 函式開啟 IMAGE_REGISTRATION_DEPTH_TO_COLOR 這項設定會是相同的。

不過 Heresy 這邊測試的結果,這個轉換的結果,都會回傳 STATUS_ERROR,無法正確進行轉換就是了…


注意事項:

  • OpenNI 2 的 VideoStream 似乎是預設開啟鏡像(mirror)的設定的~如果有需要,可以透過 VideoDtream 提供的 setMirroringEnable() 這個函式,來控制鏡像的開啟或關閉。

  • 根據深度的格式(單位)的不同,convertDepthToWorld() 轉換出來的 fXfYfZ 的值的範圍也會不同。

  • 另外,官方文件也有提到,從深度座標系統轉換到世界座標系統所需要的計算輛是相對高的,所以建議可以的話,應該盡量在深度影像座標系統上進行處理,而且盡量只轉換有需要的點。

48 thoughts on “OpenNI 2 的座標系統轉換”

  1. 博主,你好,我想问下,由深度坐标系到真实坐标系的转换具体是怎样进行的,我不大懂,还有就是,为什么我在输出真实坐标的过程中,要么x,y,z同时为0,要么同时不为0,内部有什么联系吗?或者有什么算式?期待您的回信。

  2. @ 刚蛋

    一般要轉換的話,只要像本文的範例一樣,去呼叫 convertDepthToWorld() 一點一點轉換就可以了。
    如果你是問他內部的計算,他基本上是根據 FOV 來做計算了,有興趣可以參考 OpenNI 的原始碼。

    而由於深度值為 0 在 OpenNI 代表沒有辦法取得深度資料,所以一般是建議在轉換前先去檢查,如果深度不是 0,再來做轉換。

  3. heresy大神,您好,我想得到棋盘格标定板上角点的三维坐标值。用kinect同时得到深度图和色彩图(已经处理过视角问题)。用棋盘格找角点的方法得到角点,然后在相应的深度图上得到角点的深度,然后用convertDepthToWorld函数转换到世界坐标,但是原本应该规律排列的角点,分布很乱,不成棋盘格的形状。找到的角点应该是精确度高的,然后深度的话在1m,精确度也算高的,想知道是不是convertDepthToWorld这个函数计算的误差比较大呢?

  4. @chen

    抱歉,Heresy 沒有做過這方面的測試。
    不過,基本上 PS1080 的深度值本身就會浮動,所以一定會有誤差。
    不知道你那邊的誤差是大到怎樣的程度?如果是正面對著拍攝的話,效果怎樣?

  5. heresy大神,您好,看到你写的“不過 Heresy 這邊測試的結果,這個轉換的結果,都會回傳 STATUS_ERROR,無法正確進行轉換就是了…”,说的是convertDepthToColor()这个方法,不知道你最近有没有又研究这个问题?我测试的这个函数能正常运行,但是映射效果很差,偏差很大,不知道你有没有研究?
    另外,如果不用这个函数,您怎么解决Kinect深度图像和彩色图像像素不对应的问题?

  6. @ solo

    基本上 OpenNI 2.x 有一直在改版,所以狀態也有變化,目前應該是可以用的。
    不過誤差問題,基本上就是 OpenNI 本身的限制了,如果需要更好的對應效果,可能就要自己重頭計算了。

    另外,Kinect 的部分,新版的 OpenNI 2.2 應該已經有支援了。

  7. 請問一下heresy大大:
    http://www.youtube.com/watch?feature=player_embedded&v=moyDkVulRO0
    如同您之前的這個範例影片,想請問大大幾個問題:
    1.既然有了x、y、z、fx、fy、fz,那照理說是不是只要把這些點畫出來是不是就代表畫出了Point Cloud?
    2.另外一個問題就是,想請問一下opencv是否也能畫出這種3d的圖形,不用像opengl這麼複雜的,聽說opencv也能畫出簡單一點的3d圖型,而我上網找卻幾乎都是用opengl畫出來的,而我只想要簡單一點的3d圖,目的只是想要看出Point Cloud的大略圖案而已,能否請大大解個惑?
    謝謝。

  8. @Andy

    你如果是要畫 point cloud 的話,只需要世界座標系統的 fx, fy, fz 就夠了,深度影像的 x, y 適用不到的。
    這邊有 OpenGL 的範例
    http://sdrv.ms/15kQ7OJ

    另外,OpenCV 本身應該是沒有提供 3D 繪圖的功能。
    你如果要用 OpenCV 畫的畫,就是要自己把它投影到 2D 平面上。

  9. 請問一下heresy大大關於您的OpenGL的範例,這裡面似乎少了OpenGLCamera.h這個標頭檔,還是說哪裡可以下載我找不太到,謝謝。

  10. 不好意思,我已經從heresy大大的s裡面找到了,打擾了抱歉!

  11. 請教heresy大大一個程式碼裡面的問題,’Vector3′: 找不到識別項,不知道這個該如何解決?當然我有先試了好久都試不出來所以才來麻煩大大解惑一下謝謝。
    另外有個問題:RotateSide’ : 不是 ‘SimpleCamera’ 的成員,還有MoveUp,這兩個應該是旋轉角度吧?請問這邊是大大沒有放進去SimpleCamera裡面是嗎?還是我誤會了麻煩大大解答,謝謝。

  12. 非常感謝heresy大大,我確實找錯檔案了,已經可以WORK了,非常感謝!!!

  13. 你好,heresy大大
    请问下在用OPENGL构建出世界坐标3D图后能否输出图中某一物体中心的具体三维世界坐标?也就是相对于camera的位置。比如输出图中人头的具体位置。
    有没有关于这个问题的具体教程?谢谢~

  14. to Terry

    不太確定你的問題…
    不過如果你要能找到物體的中心點,首先你要做的應該是先切割出這個物體,然後再去計算它的中心;而中心的計算方法,實際上也有幾種不同的方法,是看你自己的需要來決定的。
    如果是要人的頭部位置,建議直接使用 NiTE 追蹤到的人體骨架會比較方便。

  15. 您好~请问下如何把得到的每组FX FY FZ R G B点输出形成一个文件?
    这个文件的数据是不是就是点云?
    我看了您写的OPENNI1生成点云的方法可是差距和2好大

  16. to Tierney

    輸出成文件是看你自己要怎麼寫。
    以 C 來說,最基本的方法就是用 fostream 寫成純文字檔。

    OpenNI 1 和 OpenNI 2 在 API 上有些不一樣,所以程式寫出來也不同,但是基本概念是一樣的。

  17. 谢谢回答,解决了那个问题,可是pointcloud文件有一点不太明白,为什么深度FZ的范围只在600-1700之间,我设置mMode.setPixelFormat(PIXEL_FORMAT_DEPTH_1_MM);
    不知道那个数值是不是单位就是MM
    可是我放置的物品是在0.5到3米多之间的,用的primesense camera感觉精度应该不会只在600-1700之间,请问下这个问题是什么原因?谢谢~

  18. 您好~我用了您的那个OPENGL范例,可是每次运行返回值都是-1,并没有生成任何图像
    debug时候问题好像出在这里:
    SimpleCamera g_Camera;语句运行结束的时候就返回-1了,无法执行后边的内容,用的是primesense camera,这个问题是否和camera有关,请问应该如何解决?

  19. to Chen

    抱歉,不過你看的是哪個範例?
    如果是 OpenNI2 的範例的 03a_GL3D_Point 這個範例的話,裡面的 SimpleCamera 只是用來管理 OpenGL 環境的攝影機位置的,和實體攝影機並無關連。
    而建立物件的時候,理應不會回傳質。

    這部分應該是要麻煩你對 main() 裡面進行偵錯,看是停在哪個階段。

  20. 请问一下,转换成点云的时候,for( int y = 0; y < vfDepthFrame.getHeight() - 1; y )这里为什么要减一呢?

  21. 您好,正在学习OpenNI和Kinect,下载的那个opengl历程没有OpenGLCamera.h文件啊,很着急,因为国内做这方面的文章好少

  22. 不好意思 heresy先生
    我想詢問一下是否有代碼可以叫出kinect中特定部位的深度值?
    最近想測試是否能用截取出的深度值(手部移動)配合時間變化做出類似力道控制的效果(時間內深度變化越大=力道越強)
    抑或是Kinect SDK中有能做出其他的類似效果?

  23. to Kent_lee

    OpenNI 或 Kinect SDK 都有提供讀取深度資訊的方法,如果是要特定區域,就是要自己去決定要使用整張深度圖的哪個部分。
    比如說搭配 HandTracker 就可以偵測手的位置、然後去分析那個位置附近的深度值。

  24. heresy大大,现在想把用户从深度图中裁剪出来然后对应到RGB图像上,有个想法就是遍历深度图的每个像素,然后确定哪些像素是属于同一个用户的,再把这些像素转换成RGB信息,思来想去,好像NiTE会给每个用户分配一个ID,那这个User在深度图中的每个部位(比如手和腿)的ID是不是都是一样的?不知道这个思路对不对。然后也有查过API文档,里面提供了一个getPixels()函数,但是介绍的不太清楚,也不知道怎么使用~~麻烦heresy大大指教一下,万分感谢了!

  25. heresy大神:我现在使用的是openni2.2.0.0.33,但convertDepthToColor()的对齐误差还是很大,点对点的映射,有没有行之有效的替代方案?谢谢

  26. to hello

    OpenNI 只有提供這個介面來做處理,如果覺得準確度不夠的話,只能自己想辦法去做計算了。

  27. 非常感谢heresy的回复。不好意思,我可能没把意思表达明确。我使用的openni2.2.0.0.33,中的convertDepthToColor()进行点对点对齐, 相较于在注册模式中设置setImageRegistrationMode()函数,来使得彩色与深度图对齐。二者效果差异较大。具体是后者对齐效果好,前者明显对齐误差很大。其实我需要的精度并不高,但偏差有一个胳膊的距离,确实太大了。

  28. to hello
    根據官方的說明,這兩者的計算結果應該是要相同的。
    當然,針對不同裝置的驅動程式模組實作,可能會有不同的結果。

    個人會建議,如果 setImageRegistrationMode() 的結果可以接受、就使用這個方法吧。

  29. 老師好:
    請問目前有使用OpenGL來顯示用OpenNI2取得的(x,y,z)的教學範例嗎謝謝.

  30. heresy老師您好:
    最近使用您的03a_GL3D_Point範例
    OpenGL header 目前include這兩個#include ,#include
    遇到以下錯誤,請問該如何處理?
    謝謝老師~~

    1>pointcloud.obj : error LNK2019: 無法解析的外部符號 __imp_glutSwapBuffers 在函式 “void __cdecl display(void)” (?display@@YAXXZ) 中被參考
    1>pointcloud.obj : error LNK2019: 無法解析的外部符號 __imp_glutPostRedisplay 在函式 “void __cdecl idle(void)” (?idle@@YAXXZ) 中被參考
    1>pointcloud.obj : error LNK2019: 無法解析的外部符號 __imp_glutMainLoop 在函式 main 中被參考
    1>pointcloud.obj : error LNK2019: 無法解析的外部符號 __imp_glutSpecialFunc 在函式 main 中被參考
    1>pointcloud.obj : error LNK2019: 無法解析的外部符號 __imp_glutKeyboardFunc 在函式 main 中被參考
    1>pointcloud.obj : error LNK2019: 無法解析的外部符號 __imp_glutIdleFunc 在函式 main 中被參考
    1>pointcloud.obj : error LNK2019: 無法解析的外部符號 __imp_glutDisplayFunc 在函式 main 中被參考
    1>pointcloud.obj : error LNK2019: 無法解析的外部符號 __imp_glutInitWindowSize 在函式 main 中被參考
    1>pointcloud.obj : error LNK2019: 無法解析的外部符號 __imp_glutInitDisplayMode 在函式 main 中被參考
    1>pointcloud.obj : error LNK2019: 無法解析的外部符號 __imp___glutInitWithExit 在函式 glutInit_ATEXIT_HACK 中被參考
    1>pointcloud.obj : error LNK2019: 無法解析的外部符號 __imp___glutCreateWindowWithExit 在函式 glutCreateWindow_ATEXIT_HACK 中被參考
    1>C:UsersUSERDocumentsVisual Studio 2010Projectskinectpointcloud_kinectx64Debugpointcloud_kinect.exe : fatal error LNK1120: 11 個無法解析的外部符號

  31. to 妍霓

    Link error 通常是 lib 檔有問題、或是給的不夠多造成的。
    旖旎這邊的錯誤來說,是他沒辦法讀取到正確的 glut 的 lib 檔。

    請確認你要建置的程式是哪個版本的。從錯誤訊息來看,似乎是要建置 64 位元的 debug 版;但是你如果是用範例程式裡面的檔案的話,基本上都只有提供 32 位元的版本。
    所以要不就是改建至 32 位元的版本,不然就是要自己去下載 64 位元的 glut。

  32. heresy老師您好:
    謝謝老師的提點~
    問題解決了!:D:D
    glut64.dll glut64.lib 載點如下
    https://github.com/OpenNI/OpenNI2/blob/master/ThirdParty/GL/glut64.lib

    另外想請問老師,我想把輸出的三點(fX,fY,fZ)儲存成.pcd(Point Cloud Data)的形式,他的概念是把目前Kinect看到的畫面定格,將畫面中的點,存成.pcd嗎~

    若我想做儲存,是不是應該把(fX,fY,fZ)轉換成array,然後把每一筆資料丟到.pcd裡面嗎,謝謝老師:D

  33. heresy老師您好:
    另想請教老師
    您在03a_GL3D_Poin範例中的special key該如何使用呀
    謝謝~

  34. to 妍霓

    要把資料輸出成 pcd 的格式,除非你是要用現成的函式庫,否則你必須要自己搞清楚 pcd 的格式是怎樣,然後自己寫程式輸出成符合的格式。
    一般來說,會包含檔案格式自己定義的 header、以及特定格式的資料內容。

    glut 的 special key 請參考官方文件:
    https://www.opengl.org/resources/libraries/glut/spec3/node54.html
    在範例程式中,是設計成可以靠鍵盤的方向鍵來做旋轉。

  35. 您好:
    若我設定setPixelFormat( openni::PIXEL_FORMAT_DEPTH_1_MM );
    在最後的convertDepthToWorld( g_vsDepthStream,
    x, y, rDepth,
    &fX, &fY, &fZ );
    所產生fX fY fZ 點座標資訊的單位為毫米mm嗎??
    謝謝

  36. to 妍霓
    個人建議,這類東西,應該是可以很簡單自己測試看看的~

  37. heresy老師您好,
    請教一下關於世界坐標系轉換的問題
    convertDepthToWorld得到的和實際距離有差異,一方面是color和depth圖像有差引起的,另一方面請問您這個函數本身的計算機理是什麼,似乎沒有在openni中找到。
    改如何改善世界坐標系計算精度呢,我用的是Primesense的相機

    謝謝啦:D

  38. to Krystal

    這部分 OpenNI 並沒有在官方文件內做說明。
    不過如果想知道他的算法,建議直接參考她的原始碼。

  39. 你好~
    請問若不用openni的convertDepthToWorld函式呼叫,要如何轉成世界座標呢?有什麼方法或公式可以自己抓數據做計算嗎?
    謝謝:)

  40. to Elena

    基本上,要轉換就是要套用 OpenNI 提供的轉換公式,而最簡單的方法,就是直接呼叫 convertDepthToWorld。
    如果是想知道公式的計算方法,可以參考 OpenNI 的原始碼。
    而如果是根本不想用他的公式,那就只能自己去推導自己的公式了。

  41. 你好~我是樓上的Elena

    那有沒有sdk裡的函式也可以做到座標轉換
    因為在讀取串流或是數據時我使用的都不是openni提供的,導致我呼叫convertDepthToWorld會出現型別不符合的狀況,想請問有什麼解決的辦法嗎?
    謝謝你!

  42. to Elena

    OpenNI 提供的函示就是 convertDepthToWorld
    如果你沒有使用 OpenNI,而是使用其他的開發環境的話,請確認他是否有提供對應的轉換函式,理論上應該都會有。

    而如果是要自己寫的話,建議請參考 OpenNI 原始碼,裡面可以找到它的計算方法。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。