在Unity中實現手部跟蹤

遊資網發表於2019-08-20
本文將由遊戲開發者Jiasheng Tang分享在Unity中實現手部跟蹤的三種方法。

很多人小時候,一直夢想著使用雙手來遠端操控物體。通常,我們可以觸碰、移動、滾動和投擲物體,但我們不能像電影中的巫師或絕地大師,可以在不接觸物體的情況下操控物體。

在Unity中實現手部跟蹤

雖然這個夢想在現實生活中難以企及,但在虛擬世界中是存在可能的。

Microsoft Kinect曾非常受歡迎,因為使用者可以通過使用它,來用雙手切掉虛擬的水果。Leap Motion允許使用者以精妙的方式和虛擬物體進行互動。

這些產品都需要使用額外的硬體,費用比較昂貴。即使擁有相應裝置,普通使用者也較難實現這種體驗。如果有一種方法只使用帶有普通RGB攝像頭智慧手機,就可以跟蹤雙手,這會是多酷的體驗。

本文將介紹如何在Unity中通過使用RGB攝像頭實現手部跟蹤。

在Unity中實現手部跟蹤

學習準備

  • 學習本文,你需要掌握以下知識內容:Unity的相關知識,本文開發使用的是Unity 2018.3.14。
  • OpenCV的基礎知識。你需要在Asset Store資源商店獲取OpenCV For Unity外掛:https://assetstore.unity.com/packages/tools/integration/opencv-for-unity-21088
  • 對神經網路的基本瞭解。


手部跟蹤的方法總結

使用RGB進行手部跟蹤有三種方法:Haar級聯方法,輪廓方法和神經網路方法。

下面是對三種方法的總結:

Haar級聯方法

簡單來說,這種方法很容易實現,而且速度非常快,但缺點是非常不穩定。

Haar級聯方法是跟蹤面部的常用方法,但手和臉不同,手沒有固定的形態。如果只希望找到靜態的標準手部影象,例如:手掌向前的張手畫面,這種方法可能會起到作用。

輪廓方法

這是一種直截了當並易於實現的方法,提供很多可以定製的引數來根據用例調整,而且在計算方面的開銷也不是很大。

如果使用者在自己房間使用該方法,它會有很好的效果。但在移動裝置上,由於光線會發生變化,背景會移動,而且使用者周圍會有其他人,因此該方法沒有很理想的效果。

神經網路方法

這種方法在三種方法中有最強的穩定性,可以應對多種情形。然而,這種方法在計算方面的開銷很大。

手部跟蹤的實現

下面將介紹相應的程式碼,你可以訪問GitHub獲取完整的程式碼。https://github.com/teejs2012/Hand_Tracking_Unity3D

Haar級聯方法

Haar級聯是使用機器學習提取特徵到XML檔案的方法。

首先,我們需要獲取訓練好的XML檔案,該檔案叫palm.xml,它專門為識別手掌而訓練。

獲取訓練好的XML檔案,請訪問:
https://github.com/Balaje/OpenCV/blob/master/haarcascades/palm.xml

在下載好該檔案後,把它移動到Unity中的StreamingAssets資料夾。

載入級聯檔案時,使用以下程式碼:

  1. var cascadeFileName = Utils.getFilePath("palm.xml");

  2. cascadeDetector = new CascadeClassifier();

  3. cascadeDetector.load(cascadeFileName);
複製程式碼

執行檢測時,我們會進行一些必要的預處理過程:把影象轉換為灰度圖,並調整直方圖。然後,我們可以呼叫detectMultiScale函式。

  1. MatOfRect hands = new MatOfRect();

  2. Mat gray = new Mat(imgHeight, imgWidth, CvType.CV_8UC3);

  3. Imgproc.cvtColor(image, gray, Imgproc.COLOR_BGR2GRAY);

  4. Imgproc.equalizeHist(gray, gray);

  5. cascadeDetector.detectMultiScale(gray, hands, 1.1, 2, 0 | Objdetect.CASCADE_DO_CANNY_PRUNING | Objdetect.CASCADE_SCALE_IMAGE | Objdetect.CASCADE_FIND_BIGGEST_OBJECT, new Size(10, 10), new Size());

  6. OpenCVForUnity.CoreModule.Rect[] handsArray = hands.toArray();

  7. if (handsArray.Length != 0){

  8. //手部已經檢測到

  9. }
複製程式碼

輪廓方法

基於輪廓的方法是很直接的方法,這種方法只使用了計算機視覺,沒有使用比較特別的模型。

輪廓方法基本上有二個主要步驟:

  • 找到影象上符合人類皮膚顏色的區域。
  • 找到符合手指的輪廓形狀。


人類皮膚顏色通常是色譜的一部分,為了找到這類顏色,我們需要把顏色形式從RGB轉換為YCrCb,並檢查每個畫素是否都在該範圍內。

  1. Mat YCrCb_image = new Mat();

  2. int Y_channel = 0;

  3. int Cr_channel = 1;

  4. int Cb_channel = 2;

  5. Imgproc.cvtColor(imgMat, YCrCb_image, Imgproc.COLOR_RGB2YCrCb);

  6. var output_mask = Mat.zeros(imgWidth, imgHeight, CvType.CV_8UC1);

  7. for (int i = 0; i < YCrCb_image.rows(); i++)

  8. {

  9. for (int j = 0; j < YCrCb_image.cols(); j++)

  10. {

  11. double[] p_src = YCrCb_image.get(i, j);

  12. if (p_src[Y_channel] > 80 && p_src[Cr_channel] > 135 && p_src[Cr_channel] < 180 && p_src[Cb_channel] > 85 && p_src[Cb_channel] < 135)

  13. {

  14. output_mask.put(i, j, 255);

  15. }

  16. }
複製程式碼

最終結果是一個遮罩,符合人類皮膚顏色的畫素是白色,剩餘部分是黑色。

在獲得這個遮罩後,我們會提取遮罩的凸殼和輪廓,並檢測“瑕疵”。“瑕疵”點表示輪廓線上遠離凸殼的點。如果“瑕疵”的角度和長度等引數符合特定標準,那麼我們就知道“瑕疵”對應著手指。

在找到足夠的“手指瑕疵”時,我們會告訴系統找到了手部,根據經驗,我們把這裡的“足夠”定義為1和4之間的數量。

下圖是瑕疵點的圖示,藍線是凸殼,綠線是輪廓。

在Unity中實現手部跟蹤

  1. //在影象中找到輪廓

  2. List<MatOfPoint> contours = new List<MatOfPoint>();

  3. Imgproc.findContours(maskImage, contours, new MatOfPoint(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);



  4. var points = new MatOfPoint(contours[index].toArray());

  5. var hull = new MatOfInt();

  6. Imgproc.convexHull(points, hull, false);



  7. //找到瑕疵

  8. var defects = new MatOfInt4();

  9. Imgproc.convexityDefects(points, hull, defects);

  10. var start_points = new MatOfPoint2f();

  11. var far_points = new MatOfPoint2f();



  12. //迴圈檢查瑕疵,瞭解它是否符合條件

  13. for (int i = 0; i < defects.size().height; i++)

  14. {

  15. int ind_start = (int)defects.get(i, 0)[0];

  16. int ind_end = (int)defects.get(i, 0)[1];

  17. int ind_far = (int)defects.get(i, 0)[2];

  18. double depth = defects.get(i, 0)[3] / 256;

  19. double a = Core.norm(contours[index].row(ind_start) - contours[index].row(ind_end));

  20. double b = Core.norm(contours[index].row(ind_far) - contours[index].row(ind_start));

  21. double c = Core.norm(contours[index].row(ind_far) - contours[index].row(ind_end));

  22. double angle = Math.Acos((b * b + c * c - a * a) / (2 * b * c)) * 180.0 / Math.PI;

  23. double threshFingerLength = ((double)maskImage.height()) / 8.0;

  24. double threshAngle = 80;

  25. if (angle < threshAngle && depth > threshFingerLength)

  26. {



  27. //起點

  28. var aa = contours[index].row(ind_start);

  29. start_points.push_back(contours[index].row(ind_start));

  30. far_points.push_back(contours[index].row(ind_far));

  31. }

  32. }


  33. // 檢查詢到的瑕疵是否在範圍內

  34. if (far_points.size().height > min_defects_count && far_points.size().height < max_defects_count)

  35. {

  36. //檢測到手部

  37. }

複製程式碼

神經網路方式

在近幾年,與用於許多工的傳統解決方案相比,基於神經網路的解決方案實現了更好的效能,這在計算機視覺領域特別明顯。因為我們的任務是計算機視覺任務,所以我們自然希望使用神經網路。

實際上,目標檢測在GitHub上有許多關於手部跟蹤的優秀專案。例如:Victordibia的handtracking專案使用經典的目標檢測模型架構 - 單步檢測SSD。

下載瞭解Victordibia的handtracking專案:
https://github.com/victordibia/handtracking

單步檢測SSD因其快捷的推理速度而出名,適用於實時應用。為了進一步提升速度,該模型使用了MobileNet結構來進行調整,最後我們得到了SSDMobileNet結構,預訓練模型的大小隻有約20MB的大小。

然而,這些解決方案使用Python和TensorFlow框架依賴來編寫,我們要如何把它使用到Unity中呢?

一個熱門方法是使用TensorflowSharp,即TensorFlow框架的C#版本。但這種方法的速度對實時應用來說太慢了,幀率甚至不到1fps。

我選擇使用了OpenCV的DNN模組。該模組是OpenCVForUnity外掛的一部分,提供了一些示例場景來展示使用方法。如果想要使用自定義模型,需要執行額外的步驟來把模型轉換為OpenCV理解的格式。

在Unity中實現手部跟蹤

首先要轉換模型。OpenCV需要特別的pbtxt檔案來使模型正常載入。生成該檔案時,我們需要模型的凍結檢視,以及針對模型的管線配置。

我從這個GitHub專案獲取了二個相應的檔案frozen_inference_graph.pb和ssd_mobilenet_v1_coco.config。

轉換過程的實現,請檢視Github的程式碼,生成出來的frozen_inference_graph.pbtxt檔案也包含在其專案中。
https://github.com/teejs2012/OpenCV_DNN_with_Tensorflow_model

在準備好frozen_inference_graph.pb和frozen_inference_graph.pbtxt檔案後,我們要把它們移動到Unity中的StreamingAssets資料夾。

載入模型時,使用以下程式碼:

  1. var modelPath = Utils.getFilePath("frozen_inference_graph.pb");

  2. var configPath = Utils.getFilePath("frozen_inference_graph.pbtxt");

  3. tfDetector = Dnn.readNetFromTensorflow(modelPath, configPath);

  4. 進行檢測時,使用以下程式碼:

  5. var blob = Dnn.blobFromImage(image, 1, new Size(300, 300), new Scalar(0, 0, 0), true, false);

  6. tfDetector.setInput(blob);

  7. Mat prob = tfDetector.forward();

  8. Mat newMat = prob.reshape(1, (int)prob.total() / prob.size(3));

  9. float maxScore = 0;

  10. int scoreInd = 0;

  11. for (int i = 0; i < newMat.rows(); i++)

  12. {

  13. var score = (float)newMat.get(i, 2)[0];

  14. if (score > maxScore)

  15. {

  16. maxScore = score;

  17. scoreInd = i;

  18. }

  19. }

  20. if (maxScore > thresholdScore)

  21. {

  22. // 檢測到手部

  23. }
複製程式碼

我們要注意的一個引數是blobFromImage函式中的Size值。在傳遞到模型前,影象會調整為該數值的大小。

建議把Size設為300,因為這是模型的訓練影象的大小,但如果幀率對應用很重要,該數值可以減少為150。

手指跟蹤

你可能會注意到,我們做的這些事情有一個很大的限制:我們只能跟蹤“手部”,而手指跟蹤不包含在工具包中。

跟蹤手指的方法也很多,其中一個流行方法是使用神經網路模型從RGB影象中估算3D手部姿勢,你可以參考和下載以下程式碼:
https://github.com/Hzzone/pytorch-openpose

小結


在Unity中實現手部跟蹤的方法為大家介紹到這裡,讓我們行動起來,在虛擬世界中成為可隔空控物的絕地大師吧。

作者: Jiasheng Tang  
來源:Unity官方平臺
原地址:https://mp.weixin.qq.com/s/mAj58uK_lswMlFMcWx554g

相關文章