前言
這篇文章主要是介紹多個手部的分割,是在前面的博文:不需要骨骼跟蹤的人體手部分割 的基礎上稍加改進的。因為識別有的一個應用場合就是手勢語言識別,而手勢一般都需要人的2只手相配合完成,因此很有必要對人體的多個手部來進行分割。
實驗說明
其實本文中使用的還是OpenNI自帶的一些算法實現的,因為OpenNI中自己本身就對每個手部有一個UserID的標志,所以我們每當檢測到一只手時就可以把手的位置連同他的ID一起存下來,后面進行手勢分割時按照檢測到的不同手勢分別進行分割即可。其程序流程圖如下所示:
下面是本次實驗特別需要注意的一些細節。
OpenNI知識點總結:
一般情況下OpenNI的回調函數中都會有一個參數pCookie,該參數就可以解決前面的博文Kinect+OpenNI學習筆記之7(OpenNI自帶的類實現手部跟蹤)
中提到的一個當時沒有完美解決的問題:即回調函數與類的靜態函數,類的靜態變量這3者之間使用相互矛盾的問題。那個時候因為在回調函數中需要使用靜態成員變量,所以類中普通的非靜態成員變量是不能夠使用的,否則程序會編譯時會報錯誤。但是如果我們把這些普通變量在類中定義成了靜態變量后,這些靜態變量就屬於類本身了,並不屬於類的對象。因此該變量在類的其它成員函數中是不能夠被使用的。這樣就產生了矛盾,當時的解決方法是將這些變量不放在類中,而放在類外稱為整個工程的全局變量。雖然理論上可以解決問題,不過一個跟類有關的變量竟然不能夠放在類的內部,聽起來就像是個大笑話!這樣的封裝明顯不合理。
幸運的是,現在因為回調函數傳進來時有了pCookie這個變量,這樣我們在回調函數中可以間接使用類的非靜態成員變量了,使用這些變量既不需要定義為static類型,且也可以在類的成員函數中來進行初始化。具體方法是將某個節點(比如說手部,人體,姿勢等節點)的注冊函數RegisterGestureCallbacks()中第3個參數設置為this指針,而不是null指針。同時在具體的回調函數中,首先把pCookie指針強制轉換成COpenNI這個類的指針,然后用轉換過來的指針調用需要用到的類的成員變量。
C/C++知識點總結:
pair和map的區別:map是一個容器,容器中的第一個元素表示的是鍵值key,其它元素分別表示以后用該容器存儲數據時的數據類型。因此map中的每一條記錄的key值是不能重復的。當map定義的時候只有2個集合的時候,里面的每一條記錄可以用pair來存儲。因此可以簡單的理解一個pair對應的是一條具體的記錄,而一個map是一個存放pair的容器,並且map聲明了容器的屬性。
當在進行pair數據類型的定義時,如果其元素中的一個已經確定,另一個還不知道,則在定義的同時可以直接傳入確定的那個元素,另一個用它的數據類型后面接一個空括號即可。
在使用vector時,必須是已存在的元素才能用下標操作符進行索引。可以使用at和[]獲取指定位置的數據或者給指定位置的數據賦值。
Qt知識點總結:
在QtCreator的使用中,有時候會出現兩個尖括號在一起的情況,這時候沒有語法錯誤,但是QtCreator這個編輯環境會在你的代碼下出現個紅色的波紋,讓人看起來非常不舒服。例如:
解決方法非常簡單,即把兩個尖括號中間不要緊挨着,用一個空格號隔開一下即可,這時候紅色的波紋警告線就消失了。
實驗結果
本工程可以對多個手部進行分割,分割效果取決於OpenNI中的手部跟蹤效果。其效果截圖如下:
實驗代碼(附錄有工程code下載鏈接)
copenni.cpp:
#ifndef COPENNI_CLASS #define COPENNI_CLASS #include <XnCppWrapper.h> #include <iostream> #include <map> using namespace xn; using namespace std; class COpenNI { public: ~COpenNI() { context.Release();//釋放空間 } bool Initial() { //初始化 status = context.Init(); if(CheckError("Context initial failed!")) { return false; } context.SetGlobalMirror(true);//設置鏡像 xmode.nXRes = 640; xmode.nYRes = 480; xmode.nFPS = 30; //產生顏色node status = image_generator.Create(context); if(CheckError("Create image generator error!")) { return false; } //設置顏色圖片輸出模式 status = image_generator.SetMapOutputMode(xmode); if(CheckError("SetMapOutputMdoe error!")) { return false; } //產生深度node status = depth_generator.Create(context); if(CheckError("Create depth generator error!")) { return false; } //設置深度圖片輸出模式 status = depth_generator.SetMapOutputMode(xmode); if(CheckError("SetMapOutputMdoe error!")) { return false; } //產生手勢node status = gesture_generator.Create(context); if(CheckError("Create gesture generator error!")) { return false; } /*添加手勢識別的種類*/ gesture_generator.AddGesture("Wave", NULL); gesture_generator.AddGesture("click", NULL); gesture_generator.AddGesture("RaiseHand", NULL); gesture_generator.AddGesture("MovingHand", NULL); //產生手部的node status = hands_generator.Create(context); if(CheckError("Create hand generaotr error!")) { return false; } //產生人體node status = user_generator.Create(context); if(CheckError("Create gesturen generator error!")) { return false; } //視角校正 status = depth_generator.GetAlternativeViewPointCap().SetViewPoint(image_generator); if(CheckError("Can't set the alternative view point on depth generator!")) { return false; } //設置與手勢有關的回調函數 XnCallbackHandle gesture_cb; gesture_generator.RegisterGestureCallbacks(CBGestureRecognized, CBGestureProgress, this, gesture_cb); //設置於手部有關的回調函數 XnCallbackHandle hands_cb; hands_generator.RegisterHandCallbacks(HandCreate, HandUpdate, HandDestroy, this, hands_cb); //設置有人進入視野的回調函數 XnCallbackHandle new_user_handle; user_generator.RegisterUserCallbacks(CBNewUser, NULL, NULL, new_user_handle); user_generator.GetSkeletonCap().SetSkeletonProfile(XN_SKEL_PROFILE_ALL);//設定使用所有關節(共15個) //設置骨骼校正完成的回調函數 XnCallbackHandle calibration_complete; user_generator.GetSkeletonCap().RegisterToCalibrationComplete(CBCalibrationComplete, NULL, calibration_complete); return true; } bool Start() { status = context.StartGeneratingAll(); if(CheckError("Start generating error!")) { return false; } return true; } bool UpdateData() { status = context.WaitNoneUpdateAll(); if(CheckError("Update date error!")) { return false; } //獲取數據 image_generator.GetMetaData(image_metadata); depth_generator.GetMetaData(depth_metadata); return true; } //得到色彩圖像的node ImageGenerator& getImageGenerator() { return image_generator; } //得到深度圖像的node DepthGenerator& getDepthGenerator() { return depth_generator; } //得到人體的node UserGenerator& getUserGenerator() { return user_generator; } //得到手勢姿勢node GestureGenerator& getGestureGenerator() { return gesture_generator; } public: DepthMetaData depth_metadata; ImageMetaData image_metadata; DepthGenerator depth_generator; HandsGenerator hands_generator; std::map<XnUserID, XnPoint3D> hand_points;//為了存儲不同手的實時點而設置的 map< XnUserID, vector<XnPoint3D> > hands_track_points;//為了繪畫后面不同手部的跟蹤軌跡而設定的 private: //該函數返回真代表出現了錯誤,返回假代表正確 bool CheckError(const char* error) { if(status != XN_STATUS_OK ) { cerr << error << ": " << xnGetStatusString( status ) << endl; return true; } return false; } //手勢某個動作已經完成檢測的回調函數 static void XN_CALLBACK_TYPE CBGestureRecognized(xn::GestureGenerator &generator, const XnChar *strGesture, const XnPoint3D *pIDPosition, const XnPoint3D *pEndPosition, void *pCookie) { COpenNI *openni = (COpenNI*)pCookie; openni->hands_generator.StartTracking(*pIDPosition); } //手勢開始檢測的回調函數 static void XN_CALLBACK_TYPE CBGestureProgress(xn::GestureGenerator &generator, const XnChar *strGesture, const XnPoint3D *pPosition, XnFloat fProgress, void *pCookie) { COpenNI *openni = (COpenNI*)pCookie; openni->hands_generator.StartTracking(*pPosition); } //手部開始建立的回調函數 static void XN_CALLBACK_TYPE HandCreate(HandsGenerator& rHands, XnUserID xUID, const XnPoint3D* pPosition, XnFloat fTime, void* pCookie) { COpenNI *openni = (COpenNI*)pCookie; XnPoint3D project_pos; openni->depth_generator.ConvertRealWorldToProjective(1, pPosition, &project_pos); pair<XnUserID, XnPoint3D> hand_point_pair(xUID, XnPoint3D());//在進行pair類型的定義時,可以將第2個設置為空 hand_point_pair.second = project_pos; openni->hand_points.insert(hand_point_pair);//將檢測到的手部存入map類型的hand_points中。 pair<XnUserID, vector<XnPoint3D>> hand_track_point(xUID, vector<XnPoint3D>()); hand_track_point.second.push_back(project_pos); openni->hands_track_points.insert(hand_track_point); } //手部開始更新的回調函數 static void XN_CALLBACK_TYPE HandUpdate(HandsGenerator& rHands, XnUserID xUID, const XnPoint3D* pPosition, XnFloat fTime, void* pCookie) { COpenNI *openni = (COpenNI*)pCookie; XnPoint3D project_pos; openni->depth_generator.ConvertRealWorldToProjective(1, pPosition, &project_pos); openni->hand_points.find(xUID)->second = project_pos; openni->hands_track_points.find(xUID)->second.push_back(project_pos); } //銷毀手部的回調函數 static void XN_CALLBACK_TYPE HandDestroy(HandsGenerator& rHands, XnUserID xUID, XnFloat fTime, void* pCookie) { COpenNI *openni = (COpenNI*)pCookie; openni->hand_points.erase(openni->hand_points.find(xUID)); openni->hands_track_points.erase(openni->hands_track_points.find(xUID)); } //有人進入視野時的回調函數 static void XN_CALLBACK_TYPE CBNewUser(UserGenerator &generator, XnUserID user, void *p_cookie) { //得到skeleton的capability,並調用RequestCalibration函數設置對新檢測到的人進行骨骼校正 generator.GetSkeletonCap().RequestCalibration(user, true); } //完成骨骼校正的回調函數 static void XN_CALLBACK_TYPE CBCalibrationComplete(SkeletonCapability &skeleton, XnUserID user, XnCalibrationStatus calibration_error, void *p_cookie) { if(calibration_error == XN_CALIBRATION_STATUS_OK) { skeleton.StartTracking(user);//骨骼校正完成后就開始進行人體跟蹤了 } else { UserGenerator *p_user = (UserGenerator*)p_cookie; skeleton.RequestCalibration(user, true);//骨骼校正失敗時重新設置對人體骨骼繼續進行校正 } } private: XnStatus status; Context context; ImageGenerator image_generator; UserGenerator user_generator; GestureGenerator gesture_generator; XnMapOutputMode xmode; }; #endif
main.cpp:
#include <iostream> #include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <opencv2/core/core.hpp> #include "copenni.cpp" #include <iostream> #define DEPTH_SCALE_FACTOR 255./4096. #define ROI_HAND_WIDTH 140 #define ROI_HAND_HEIGHT 140 #define MEDIAN_BLUR_K 5 int XRES = 640; int YRES = 480; #define DEPTH_SEGMENT_THRESH 5 using namespace cv; using namespace xn; using namespace std; int main (int argc, char **argv) { COpenNI openni; vector<Scalar> color_array;//采用默認的10種顏色 { color_array.push_back(Scalar(255, 0, 0)); color_array.push_back(Scalar(0, 255, 0)); color_array.push_back(Scalar(0, 0, 255)); color_array.push_back(Scalar(255, 0, 255)); color_array.push_back(Scalar(255, 255, 0)); color_array.push_back(Scalar(0, 255, 255)); color_array.push_back(Scalar(128, 255, 0)); color_array.push_back(Scalar(0, 128, 255)); color_array.push_back(Scalar(255, 0, 128)); color_array.push_back(Scalar(255, 128, 255)); } vector<int> hand_depth(10, 0); vector<Rect> hands_roi(10, Rect(320, 240, ROI_HAND_WIDTH, ROI_HAND_HEIGHT)); if(!openni.Initial()) return 1; namedWindow("color image", CV_WINDOW_AUTOSIZE); namedWindow("depth image", CV_WINDOW_AUTOSIZE); namedWindow("hand_segment", CV_WINDOW_AUTOSIZE);//顯示分割出來的手的區域 if(!openni.Start()) return 1; while(1) { if(!openni.UpdateData()) { return 1; } /*獲取並顯示色彩圖像*/ Mat color_image_src(openni.image_metadata.YRes(), openni.image_metadata.XRes(), CV_8UC3, (char *)openni.image_metadata.Data()); Mat color_image; cvtColor(color_image_src, color_image, CV_RGB2BGR); for(auto itUser = openni.hand_points.cbegin(); itUser != openni.hand_points.cend(); ++itUser) { circle(color_image, Point(itUser->second.X, itUser->second.Y), 5, color_array.at(itUser->first % color_array.size()), 3, 8); /*設置不同手部的深度*/ hand_depth.at(itUser->first) = itUser->second.Z* DEPTH_SCALE_FACTOR; /*設置不同手部的不同感興趣區域*/ hands_roi.at(itUser->first) = Rect(itUser->second.X - ROI_HAND_WIDTH/2, itUser->second.Y - ROI_HAND_HEIGHT/2, ROI_HAND_WIDTH, ROI_HAND_HEIGHT); hands_roi.at(itUser->first).x = itUser->second.X - ROI_HAND_WIDTH/2; hands_roi.at(itUser->first).y = itUser->second.Y - ROI_HAND_HEIGHT/2; hands_roi.at(itUser->first).width = ROI_HAND_WIDTH; hands_roi.at(itUser->first).height = ROI_HAND_HEIGHT; if(hands_roi.at(itUser->first).x <= 0) hands_roi.at(itUser->first).x = 0; if(hands_roi.at(itUser->first).x > XRES) hands_roi.at(itUser->first).x = XRES; if(hands_roi.at(itUser->first).y <= 0) hands_roi.at(itUser->first).y = 0; if(hands_roi.at(itUser->first).y > YRES) hands_roi.at(itUser->first).y = YRES; } imshow("color image", color_image); /*獲取並顯示深度圖像*/ Mat depth_image_src(openni.depth_metadata.YRes(), openni.depth_metadata.XRes(), CV_16UC1, (char *)openni.depth_metadata.Data());//因為kinect獲取到的深度圖像實際上是無符號的16位數據 Mat depth_image; depth_image_src.convertTo(depth_image, CV_8U, DEPTH_SCALE_FACTOR); imshow("depth image", depth_image); //取出手的mask部分 //不管原圖像時多少通道的,mask矩陣聲明為單通道就ok Mat hand_segment_mask(depth_image.size(), CV_8UC1, Scalar::all(0)); for(auto itUser = openni.hand_points.cbegin(); itUser != openni.hand_points.cend(); ++itUser) for(int i = hands_roi.at(itUser->first).x; i < std::min(hands_roi.at(itUser->first).x+hands_roi.at(itUser->first).width, XRES); i++) for(int j = hands_roi.at(itUser->first).y; j < std::min(hands_roi.at(itUser->first).y+hands_roi.at(itUser->first).height, YRES); j++) { hand_segment_mask.at<unsigned char>(j, i) = ((hand_depth.at(itUser->first)-DEPTH_SEGMENT_THRESH) < depth_image.at<unsigned char>(j, i)) & ((hand_depth.at(itUser->first)+DEPTH_SEGMENT_THRESH) > depth_image.at<unsigned char>(j,i)); } medianBlur(hand_segment_mask, hand_segment_mask, MEDIAN_BLUR_K); Mat hand_segment(color_image.size(), CV_8UC3); color_image.copyTo(hand_segment, hand_segment_mask); imshow("hand_segment", hand_segment); waitKey(30); } }
實驗總結:
本次實驗基本上都是基於OpenNI的一些算法的,其分割也是簡單的用深度信息進行,沒有什么特別的算法,且分割過程中沒有引入色彩信息。后續的工作是從將色彩信息和深度信息結合起來做分割,即達到手部分割鄰域自適應選擇。
附錄:實驗工程code。