錄製、重播 OpenNI 擷取到的資料


前面已經花了好幾篇文章,來講怎樣透過 OpenNI 來讀取 Kinect 的資料了,而接下來這一篇,則是大概來講一下,怎樣把讀到的資料記錄下來。

在簡介 OpenNI 的時候(文章),已經有提到過,OpenNI 裡提供錄製和撥放功能相關的 Production Node,分別是 Recorder、Player、Codec;而透過 OpenNI 所設計的架構,其實要錄製、或是使用錄製下來的資料,其實都是相當簡單的~不管是在《OpenNI User Guide》或是《OpenNI Documentation》裡,都有提供範例可以參考。

檔案格式

而以 OpenNI 提供的方法來進行錄製的話,他會把指定 node 的資料,都錄製在一個特殊格式「ONI」的檔案裡;這個格式他裡面包含了複數個 node 的資料,所以只要一個檔案,就可以同時記錄深度和彩色影像了。

而實際上,OpenNI 官方範例裡的 NiViewer 本身就包括了這樣的錄製(程式執行後按鍵盤「s」)、以及撥放的功能(直接點 *.ONI 應該就會由 NiViewer 開啟了)。

錄製

首先,下面就是一個簡單的錄製深度以及彩色影像的程式碼:

// 1. initial context
xn::Context mContext;
mContext.Init();
 
// 2. map output mode
XnMapOutputMode mapMode;
mapMode.nXRes = 640;
mapMode.nYRes = 480;
mapMode.nFPS = 30;
 
// 3. create image generator
xn::ImageGenerator mImageGen;
mImageGen.Create( mContext );
mImageGen.SetMapOutputMode( mapMode );
 
// 4. create depth generator
xn::DepthGenerator mDepthGen;
mDepthGen.Create( mContext );
mDepthGen.SetMapOutputMode( mapMode );
mDepthGen.GetAlternativeViewPointCap().SetViewPoint( mImageGen );

// 5. create recorder xn::Recorder mRecoder; mRecoder.Create( mContext ); mRecoder.SetDestination( XN_RECORD_MEDIUM_FILE, sFilename.c_str() ); mRecoder.AddNodeToRecording( mImageGen, XN_CODEC_JPEG ); mRecoder.AddNodeToRecording( mDepthGen, XN_CODEC_16Z_EMB_TABLES );
// 6. start generate data mContext.StartGeneratingAll(); // 7. loop unsigned int i = 0; while( true ) { if( ++i > 1000 ) break; cout << i << endl; mContext.WaitAndUpdateAll(); }
mRecoder.Release();
// 8. stop mContext.StopGeneratingAll(); mContext.Shutdown();

在上面的程式碼裡,應該可以很簡單地看的出來,大部分的程式其實都和《透過 OpneNI 合併 Kinect 深度以及彩色影像資料》一文內的程式碼差不多,主要有所不同的地方,僅有用黃色強調的部分而已;而實際上,要讓現有的 OpenNI 程式能夠把資料錄製下來,也就只要加上這些程式就夠了!

究竟要加那些東西呢?其實真的很簡單,只需要在本來的程式的 node 都設定完成後,在開始建立 xn::Recorder 這個錄製資料用的 node,並進行設定就可以了~對應到上面的程式碼,就是「5. create recorder」的部分。

這裡,首先就是宣告出一個 xn::Recorder 的物件 mRecorder,並透過 Create() 這個函式來建立出 production node。接下來,則是透過 SetDestination() 這個函式,來指定 recorder 之後要將資料存在哪個檔案裡;而這個函式需要指定兩個參數,第一個是要錄製的媒體形式,目前 OpenNI 僅能使用 XN_RECORD_MEDIUM_FILE、也就是檔案的形式,而沒有其他選擇。第二個參數則就是檔案的名稱,也就是要錄製的檔案名稱(這邊 sFilename 的型別是 std::string)。

在這些設定好了以後,接下來就是要設定要錄製的 node 了,這邊所使用的是 AddNodeToRecording() 這個函式。這個函式在使用上也很單純,第一個參數就是要錄製的 node,在這邊就是 mImageGenmDepthGen 這兩個 node;而第二個參數,則是要用什麼樣的方法,來對這個 node 的資料進行壓縮。

在壓縮的 codec 來說,OpenNI 提供了幾種選擇:

  • XN_CODEC_NULL:採用 node 預設值
  • XN_CODEC_UNCOMPRESSED:不壓縮
  • XN_CODEC_JPEG:JPEG 壓縮(有損)
  • XN_CODEC_16ZXN_CODEC_16Z_EMB_TABLESXN_CODEC_8Z:ZIP 壓縮?

不過很遺憾的是,在官方資料裡面,似乎沒有針對這些 codec 做詳細的說明,所以 Heresy 不太確定最後三個壓縮方法的意義,不過以字面上來看,Heresy 個人覺得應該是針對 16bit 和 8bit 資料做 ZIP 壓縮;而以 NiViewer 裡面的程式來看,一般 image generator 應該是可以使用 XN_CODEC_JPEG 來做壓縮,而 depth generator 則是使用 XN_CODEC_16Z_EMB_TABLES。而如果不在乎空間的話,要使用 XN_CODEC_UNCOMPRESSED 應該也是可行的。

xn::Recorder 都設定好了之後,接下來就可以按照原來的方法,透過 context 的 StartGeneratingAll() 來開始讀取資料了~而之後呢,只要呼叫了 context 的 WaitAndUpdateAll() 這系列的函式,xn::Recorder 就會把資料寫入到指定的 ONI 檔裡了~(或者,也可以呼叫 xn::RecorderRecord() 函式)

而 Heresy 在這邊,則是用一個迴圈來做資料讀取、錄製的動作,這個迴圈會重複 1000 次,然後結束。在結束時,似乎是需要呼叫 xn::RecorderRelease() 函式、來釋放本身的資源、完成檔案寫出的動作。不過這邊可能要注意的是,目前 OpenNI 的 recorder 雖然可以持續的紀錄資料在 ONI 檔中,但是在檔案大小超過一定大小(約 2GB)後,會無法完成最後寫入的動作,導致最後的檔案無法被正常存取;所以如果要長時間錄製的話,可能就要自己注意檔案的大小、考慮想辦法切割檔案了。

 

播放

在播放的部分,如果只是單純要使用錄製下來的 ONI 的話,也相當地簡單,原有的程式只要加上一行就可以了!而如果要有額外的控制功能,也可以透過 OpenNI 的 xn::Player 來做到。

最簡單的其修改方法,就是在 OpenNI 的 context 初始化完成後、建立 Production Node 前,加上一行 xn::Context::OpenFileRecording(),來開起一個 ONI 檔。如果以《透過 OpneNI 合併 Kinect 深度以及彩色影像資料》一文中的程式來說,就是把「2. initial context」的部分,修改為:

xn::Context mContext;
eRes = mContext.Init();
mContext.OpenFileRecording( sFilename.c_str() );

接下來,就繼續沿用原來的程式碼就可以了!

而在透過 mContextOpenFileRecording() 去開啟 sFilename(型別為 std::string)這個檔案後,Conext 內部會由讀取實際的裝置(Kinect)、改成去讀取 ONI 檔內的資料,而之後所建立的 Production Node,也都會對應到 ONI 內所錄製的 node。而之後讀取的時候,也是會依序讀出 ONI 檔裡各 node 的資料,不會再去讀取實際的裝置。

不過也由於這邊的資料都是已經記錄好的,所以在設定上會有一些限制,像是 Alternative View 在這個情況下,就會無法使用;不過由於 OpenNI 的設計上並不會因為這類的函示無法呼叫,就強制中斷,所以程式還是可以可以繼續執行的~但是如果去分析每一個步驟回傳的 XnStatus 的話,就可以看的出來那些函示無法在播放 ONI 時使用了。

另外,如果希望針對撥放在做進一步的控制的話,也可以使用 xn::Player 這個 node 來做操作,包括了跳到某個 frame、重播、撥放速度等等,都可以做控制。不過這部分在這邊就暫時不提,以後如果有機會再寫了。

 

這篇基本上就先到這了。而有了錄製和撥放的功能後,基本上測試資料就可以很簡單地記錄下來,要針對特殊的狀況來幫程式除錯,也會變得比較簡單了~


OpenNI / Kinect 相關文章目錄

對「錄製、重播 OpenNI 擷取到的資料」的想法

  1. 請問Heresy老師,關於code中的sFilename要如何宣告呢?
    我的宣告方式為string sFilename[]="123.oni";
    但仍然編譯錯誤。

    openNI僅能錄製和播放.oni 嗎?
    是否能儲存成.pcd 呢?

  2. 我嘗試以上程式碼,並自己錄製影片!
    可是再次想 mContext.OpenFileRecording( sFilename.c_str() ); 卻無法讀入自己錄製的影片!
    確定openNI的範例程式NiViewer所錄製的影片沒問題!可以讀取的了~
    也確定假如沒有記錄檔,會主動換去抓取Kinect的資料!

    • 根據之前的經驗,OpenNI 的 Recorder 沒有正常結束的話,錄出來的檔案可能會無法使用。
      不知道有沒有可能是這個問題?

      • 是我沒好好看H大寫的!H大有寫~
        而 Heresy 在這邊,則是用一個迴圈來做資料讀取、錄製的動作,這個迴圈會重複 1000 次,然後結束。在結束時,似乎是需要呼叫 xn::Recorder 的 Release() 函式、來釋放本身的資源、完成檔案寫出的動作。

    • 因為本身直接中途中斷,沒有執行Release而導致影片沒完成檔案寫出的動作!
      假如要中途中斷也要記得Release,不然會和我一樣!

  3. Heresy你好

    想請問一下 recorder的create函數不包含query參數,那當我要同時錄多台kinect資訊時,該如何處理?

    謝謝你了!!

    • 我好像已經找到方法去存了,自己建多個 Xn::Recorder 去錄影。
      好像就沒辦法跟Depth還有Image的Generator一樣!!!
      :D

    • Recorder 的設計理念本來就是建立出來後,再去指定要錄那些 node,所以只要在建立出來後,再去指定要錄哪個 node 就可以了。

    • 對了Heresy老師,再看Boost 的timer 時看到另外有人說time.h裡面的clock()函數是否也可以達到您所說的效果呢?
      剛查了跟試了一下,是可以顯示程式所跑過的時間

    • 剛剛又花了點時間搞懂Boost的Timer,等我是完再跟老師報告一下
      感覺Boost有好多很好用的東西

  4. 不好意思,又來發問了
    請問Heresy老師,最近看了您之前介紹有趣的Kinect應用
    https://kheresy.wordpress.com/2012/07/22/funny-application-of-kinect-or-openni/
    裡面有看到了Kinetic Space這個軟體,我也下載來玩過了
    它裡面是可以判定你現在在做的動作跟他內建的動作的符合度以判定你現在是做什麼動作
    如果要做到像他那樣,"事先定義"好一個動作,再去跟現在做的動作做比較
    是否要用您這篇文章所說的,先錄製好動作,再去跟現在所做的動作做比較呢
    因為目前我在使用的方法都是單純判定各個節點位置的移動跟距離,所以很容易就觸發到其他動作
    想試看看有沒有辦法可以向內建的手勢"動作"那樣
    不知道Heresy老師了解我的意思嗎(感覺我講的好模糊= = )

    • 姿勢、動作辨識的問題,在 Heresy 來看基本上,這邊應該會比較接近 machine learning 的問題。最後的重點還是看要取哪些資訊當作特徵、用什麼方法來做分類。

      Kinetic Space 的方法,個人猜測應該是採取分類(classification)的方法,來判斷他和哪種現有已定義的動作比較像來做判斷的。
      而實務上,就算是根據現有、已經錄好的動作來比較,真正要處理的時候,還是要先定義出比較的標準、以及判斷的條件;所以實際上,錄影的動作只算是一種資料的取得、定義的方法而已,並非必要,也不一定比較好。

      • 還是要先謝謝Heresy老師的解釋(老師你回覆好快!!!),因為我們專題是要利用Kinect去操作
        所以很多都是從Heresy老師的文章來慢慢學習的
        因為動作有點多,所以有時候要做某個動作時,會不小心觸發到另一個動作
        目前我們是利用開關的方法,比如說,右手摸著頭的話,所做的動作就只會觸發某一個動作的指令
        然後摸著肚子的話又只會觸發另一套動作的指令
        避免觸發到不同套動作的指令
        因為Kinect在讀身體節點資料的速度很快
        像是比如說我要判定他有沒有將右手快速從右邊往左邊揮動
        我直接取他右手的X位置,前一次跟這一次相減,若大於某個值就判定為是
        但是有時候會有些不靈敏或誤差
        除了多加一些條件以外(比如說多加一項右手還要從頭的右邊變到左邊)
        但這又有可能變成節點資料讀的不夠精確,手揮了好幾次還是沒有觸發函式(每次測試時手都超酸的哈哈)
        上網查關於這些的資料實在很少
        想問Heresy老師
        是否有更好的方法呢??

        • 個人會建議,不要只用瞬間的空間資訊做判斷,最好也把時間加入考量。
          例如摸頭的動作,可以設計成要摸了 0.5 秒後才觸發;甚至是用這 0.5 秒內,摸頭的時間比例大於多少來做判斷,這樣可以避免資料忽然跳耀的問題。

          而向你說要判斷從頭的左邊移到右邊,基本上也是不建議用連續兩次資料更新的間隔來做,而是考慮在多少時間內,手從頭的左邊移到右邊,這樣應該會好一點。

          另外,測試的話,其實可以先把做幾次測試動作,錄成 ONI 檔;然後透過播放的方法來做測試。

          • 謝謝Heresy老師!!!!!這方法的意思是
            看kinect他一秒鐘更新幾次資料
            比如說一秒鐘更新20次
            那就在更新60次(也就是3秒)之後來比對第61次跟第1次紀錄時的差距嗎???
            理解力有點差對不起……

            因為之前一直不知道要怎麼用"時間"去計算
            是要利用其他的函式嗎

            老師完全了解我的盲點耶!!!!!
            資料因為會忽然跳躍所以每次都會有誤差
            比如說明明揮了很大一下,他還是沒有觸發(超累的!!!!!)
            有時候揮了一小下,他就成功觸發……..
            辛苦老師了!!!!!!!

          • 個人不建議用更新次數當時間。
            因為更新的速度,很有可能會因為處理的過程、而造成延遲、或者掉 frame。比較好的方法,是透過計時器的方法,來取得比較穩定的時間差。

            例如 Boost C++ Libraries 就有提供 Timer 的函式庫
            http://www.boost.org/doc/libs/1_50_0/libs/timer/doc/index.html

            如果是 C++ 11 的標準的話,在標準 STL 裡也有提供 chrono 這個函式庫,可以做http://www.cplusplus.com/reference/std/chrono/

  5. […] 這是包含在 Kinect for Windows Toolkit 裡面的新工具,可以用來錄製、重播 Kinect 的資料;基本上,應該主要就是拿來產生、使用測試資料的工具了~使用的說明,可以參考 MSDN 上的文件。 (這項功能在 OpenNI 其實早就有了,可以參考《錄製、重播 OpenNI 擷取到的資料》) […]

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com 標誌

您的留言將使用 WordPress.com 帳號。 登出 /  變更 )

Google photo

您的留言將使用 Google 帳號。 登出 /  變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 /  變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 /  變更 )

連結到 %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.