在之前的《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() 這個函式,也就是上方黃底的部分。而呼叫這個函式時,需要的參數包含了三個部分:
- 儲存轉換資訊的深度感應器的 VideoStream:vsDepthStream
- 輸入的資料、也就深度座標系統的點位:x、y、rDepth 
- 輸出的結果,也就世界座標系統的點位:fX、fY、fZ
上面的程式執行完之後,( 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() 轉換出來的 fX、fY、fZ 的值的範圍也會不同。
-
另外,官方文件也有提到,從深度座標系統轉換到世界座標系統所需要的計算輛是相對高的,所以建議可以的話,應該盡量在深度影像座標系統上進行處理,而且盡量只轉換有需要的點。
博主,你好,我想问下,由深度坐标系到真实坐标系的转换具体是怎样进行的,我不大懂,还有就是,为什么我在输出真实坐标的过程中,要么x,y,z同时为0,要么同时不为0,内部有什么联系吗?或者有什么算式?期待您的回信。
@ 刚蛋
一般要轉換的話,只要像本文的範例一樣,去呼叫 convertDepthToWorld() 一點一點轉換就可以了。
如果你是問他內部的計算,他基本上是根據 FOV 來做計算了,有興趣可以參考 OpenNI 的原始碼。
而由於深度值為 0 在 OpenNI 代表沒有辦法取得深度資料,所以一般是建議在轉換前先去檢查,如果深度不是 0,再來做轉換。
heresy大神,您好,我想得到棋盘格标定板上角点的三维坐标值。用kinect同时得到深度图和色彩图(已经处理过视角问题)。用棋盘格找角点的方法得到角点,然后在相应的深度图上得到角点的深度,然后用convertDepthToWorld函数转换到世界坐标,但是原本应该规律排列的角点,分布很乱,不成棋盘格的形状。找到的角点应该是精确度高的,然后深度的话在1m,精确度也算高的,想知道是不是convertDepthToWorld这个函数计算的误差比较大呢?
@chen
抱歉,Heresy 沒有做過這方面的測試。
不過,基本上 PS1080 的深度值本身就會浮動,所以一定會有誤差。
不知道你那邊的誤差是大到怎樣的程度?如果是正面對著拍攝的話,效果怎樣?
heresy大神,您好,看到你写的“不過 Heresy 這邊測試的結果,這個轉換的結果,都會回傳 STATUS_ERROR,無法正確進行轉換就是了…”,说的是convertDepthToColor()这个方法,不知道你最近有没有又研究这个问题?我测试的这个函数能正常运行,但是映射效果很差,偏差很大,不知道你有没有研究?
另外,如果不用这个函数,您怎么解决Kinect深度图像和彩色图像像素不对应的问题?
@ solo
基本上 OpenNI 2.x 有一直在改版,所以狀態也有變化,目前應該是可以用的。
不過誤差問題,基本上就是 OpenNI 本身的限制了,如果需要更好的對應效果,可能就要自己重頭計算了。
另外,Kinect 的部分,新版的 OpenNI 2.2 應該已經有支援了。
請問一下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的大略圖案而已,能否請大大解個惑?
謝謝。
@Andy
你如果是要畫 point cloud 的話,只需要世界座標系統的 fx, fy, fz 就夠了,深度影像的 x, y 適用不到的。
這邊有 OpenGL 的範例
http://sdrv.ms/15kQ7OJ
另外,OpenCV 本身應該是沒有提供 3D 繪圖的功能。
你如果要用 OpenCV 畫的畫,就是要自己把它投影到 2D 平面上。
請問一下heresy大大關於您的OpenGL的範例,這裡面似乎少了OpenGLCamera.h這個標頭檔,還是說哪裡可以下載我找不太到,謝謝。
不好意思,我已經從heresy大大的s裡面找到了,打擾了抱歉!
請教heresy大大一個程式碼裡面的問題,’Vector3′: 找不到識別項,不知道這個該如何解決?當然我有先試了好久都試不出來所以才來麻煩大大解惑一下謝謝。
另外有個問題:RotateSide’ : 不是 ‘SimpleCamera’ 的成員,還有MoveUp,這兩個應該是旋轉角度吧?請問這邊是大大沒有放進去SimpleCamera裡面是嗎?還是我誤會了麻煩大大解答,謝謝。
@Andy
你可能找錯檔案了,檔案是這個:
http://sdrv.ms/151gCX9
麻煩確認一下,裡面應該是有定義 Vector3 這個類別的。
非常感謝heresy大大,我確實找錯檔案了,已經可以WORK了,非常感謝!!!
你好,heresy大大
请问下在用OPENGL构建出世界坐标3D图后能否输出图中某一物体中心的具体三维世界坐标?也就是相对于camera的位置。比如输出图中人头的具体位置。
有没有关于这个问题的具体教程?谢谢~
to Terry
不太確定你的問題…
不過如果你要能找到物體的中心點,首先你要做的應該是先切割出這個物體,然後再去計算它的中心;而中心的計算方法,實際上也有幾種不同的方法,是看你自己的需要來決定的。
如果是要人的頭部位置,建議直接使用 NiTE 追蹤到的人體骨架會比較方便。
您好~请问下如何把得到的每组FX FY FZ R G B点输出形成一个文件?
这个文件的数据是不是就是点云?
我看了您写的OPENNI1生成点云的方法可是差距和2好大
to Tierney
輸出成文件是看你自己要怎麼寫。
以 C 來說,最基本的方法就是用 fostream 寫成純文字檔。
OpenNI 1 和 OpenNI 2 在 API 上有些不一樣,所以程式寫出來也不同,但是基本概念是一樣的。
谢谢回答,解决了那个问题,可是pointcloud文件有一点不太明白,为什么深度FZ的范围只在600-1700之间,我设置mMode.setPixelFormat(PIXEL_FORMAT_DEPTH_1_MM);
不知道那个数值是不是单位就是MM
可是我放置的物品是在0.5到3米多之间的,用的primesense camera感觉精度应该不会只在600-1700之间,请问下这个问题是什么原因?谢谢~
您好~我用了您的那个OPENGL范例,可是每次运行返回值都是-1,并没有生成任何图像
debug时候问题好像出在这里:
SimpleCamera g_Camera;语句运行结束的时候就返回-1了,无法执行后边的内容,用的是primesense camera,这个问题是否和camera有关,请问应该如何解决?
to Chen
抱歉,不過你看的是哪個範例?
如果是 OpenNI2 的範例的 03a_GL3D_Point 這個範例的話,裡面的 SimpleCamera 只是用來管理 OpenGL 環境的攝影機位置的,和實體攝影機並無關連。
而建立物件的時候,理應不會回傳質。
這部分應該是要麻煩你對 main() 裡面進行偵錯,看是停在哪個階段。
请问一下,转换成点云的时候,for( int y = 0; y < vfDepthFrame.getHeight() - 1; y )这里为什么要减一呢?
to Wang zhiyong
應該只是純粹貼錯程式碼而已 XD
您好,正在学习OpenNI和Kinect,下载的那个opengl历程没有OpenGLCamera.h文件啊,很着急,因为国内做这方面的文章好少
to Super
Heresy 都有放在 SkyDrive 上,請自行下載
http://sdrv.ms/151gCX9
不好意思 heresy先生
我想詢問一下是否有代碼可以叫出kinect中特定部位的深度值?
最近想測試是否能用截取出的深度值(手部移動)配合時間變化做出類似力道控制的效果(時間內深度變化越大=力道越強)
抑或是Kinect SDK中有能做出其他的類似效果?
to Kent_lee
OpenNI 或 Kinect SDK 都有提供讀取深度資訊的方法,如果是要特定區域,就是要自己去決定要使用整張深度圖的哪個部分。
比如說搭配 HandTracker 就可以偵測手的位置、然後去分析那個位置附近的深度值。
heresy大大,现在想把用户从深度图中裁剪出来然后对应到RGB图像上,有个想法就是遍历深度图的每个像素,然后确定哪些像素是属于同一个用户的,再把这些像素转换成RGB信息,思来想去,好像NiTE会给每个用户分配一个ID,那这个User在深度图中的每个部位(比如手和腿)的ID是不是都是一样的?不知道这个思路对不对。然后也有查过API文档,里面提供了一个getPixels()函数,但是介绍的不太清楚,也不知道怎么使用~~麻烦heresy大大指教一下,万分感谢了!
不好意思,之前把名字打成您的了~
to gostfly
建議可以參考這邊的範例:
https://onedrive.live.com/?cid=E0070FB8ECF9015F&id=E0070FB8ECF9015F!14206&authkey=!APILH8MLu-cra3E
heresy大神:我现在使用的是openni2.2.0.0.33,但convertDepthToColor()的对齐误差还是很大,点对点的映射,有没有行之有效的替代方案?谢谢
to hello
OpenNI 只有提供這個介面來做處理,如果覺得準確度不夠的話,只能自己想辦法去做計算了。
非常感谢heresy的回复。不好意思,我可能没把意思表达明确。我使用的openni2.2.0.0.33,中的convertDepthToColor()进行点对点对齐, 相较于在注册模式中设置setImageRegistrationMode()函数,来使得彩色与深度图对齐。二者效果差异较大。具体是后者对齐效果好,前者明显对齐误差很大。其实我需要的精度并不高,但偏差有一个胳膊的距离,确实太大了。
to hello
根據官方的說明,這兩者的計算結果應該是要相同的。
當然,針對不同裝置的驅動程式模組實作,可能會有不同的結果。
個人會建議,如果 setImageRegistrationMode() 的結果可以接受、就使用這個方法吧。
老師好:
請問目前有使用OpenGL來顯示用OpenNI2取得的(x,y,z)的教學範例嗎謝謝.
to mommy
請參考:
http://viml.nchc.org.tw/blog/paper_info.php?CLASS_ID=1&SUB_ID=1&PAPER_ID=449
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 個無法解析的外部符號
to 妍霓
Link error 通常是 lib 檔有問題、或是給的不夠多造成的。
旖旎這邊的錯誤來說,是他沒辦法讀取到正確的 glut 的 lib 檔。
請確認你要建置的程式是哪個版本的。從錯誤訊息來看,似乎是要建置 64 位元的 debug 版;但是你如果是用範例程式裡面的檔案的話,基本上都只有提供 32 位元的版本。
所以要不就是改建至 32 位元的版本,不然就是要自己去下載 64 位元的 glut。
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
heresy老師您好:
另想請教老師
您在03a_GL3D_Poin範例中的special key該如何使用呀
謝謝~
to 妍霓
要把資料輸出成 pcd 的格式,除非你是要用現成的函式庫,否則你必須要自己搞清楚 pcd 的格式是怎樣,然後自己寫程式輸出成符合的格式。
一般來說,會包含檔案格式自己定義的 header、以及特定格式的資料內容。
glut 的 special key 請參考官方文件:
https://www.opengl.org/resources/libraries/glut/spec3/node54.html
在範例程式中,是設計成可以靠鍵盤的方向鍵來做旋轉。
您好:
若我設定setPixelFormat( openni::PIXEL_FORMAT_DEPTH_1_MM );
在最後的convertDepthToWorld( g_vsDepthStream,
x, y, rDepth,
&fX, &fY, &fZ );
所產生fX fY fZ 點座標資訊的單位為毫米mm嗎??
謝謝
to 妍霓
個人建議,這類東西,應該是可以很簡單自己測試看看的~
heresy老師您好,
請教一下關於世界坐標系轉換的問題
convertDepthToWorld得到的和實際距離有差異,一方面是color和depth圖像有差引起的,另一方面請問您這個函數本身的計算機理是什麼,似乎沒有在openni中找到。
改如何改善世界坐標系計算精度呢,我用的是Primesense的相機
謝謝啦:D
to Krystal
這部分 OpenNI 並沒有在官方文件內做說明。
不過如果想知道他的算法,建議直接參考她的原始碼。
你好~
請問若不用openni的convertDepthToWorld函式呼叫,要如何轉成世界座標呢?有什麼方法或公式可以自己抓數據做計算嗎?
謝謝:)
to Elena
基本上,要轉換就是要套用 OpenNI 提供的轉換公式,而最簡單的方法,就是直接呼叫 convertDepthToWorld。
如果是想知道公式的計算方法,可以參考 OpenNI 的原始碼。
而如果是根本不想用他的公式,那就只能自己去推導自己的公式了。
你好~我是樓上的Elena
那有沒有sdk裡的函式也可以做到座標轉換
因為在讀取串流或是數據時我使用的都不是openni提供的,導致我呼叫convertDepthToWorld會出現型別不符合的狀況,想請問有什麼解決的辦法嗎?
謝謝你!
to Elena
OpenNI 提供的函示就是 convertDepthToWorld
如果你沒有使用 OpenNI,而是使用其他的開發環境的話,請確認他是否有提供對應的轉換函式,理論上應該都會有。
而如果是要自己寫的話,建議請參考 OpenNI 原始碼,裡面可以找到它的計算方法。