在Unity中實現手部跟蹤
很多人小時候,一直夢想著使用雙手來遠端操控物體。通常,我們可以觸碰、移動、滾動和投擲物體,但我們不能像電影中的巫師或絕地大師,可以在不接觸物體的情況下操控物體。
雖然這個夢想在現實生活中難以企及,但在虛擬世界中是存在可能的。
Microsoft Kinect曾非常受歡迎,因為使用者可以通過使用它,來用雙手切掉虛擬的水果。Leap Motion允許使用者以精妙的方式和虛擬物體進行互動。
這些產品都需要使用額外的硬體,費用比較昂貴。即使擁有相應裝置,普通使用者也較難實現這種體驗。如果有一種方法只使用帶有普通RGB攝像頭智慧手機,就可以跟蹤雙手,這會是多酷的體驗。
本文將介紹如何在Unity中通過使用RGB攝像頭實現手部跟蹤。
學習準備
- 學習本文,你需要掌握以下知識內容: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資料夾。
載入級聯檔案時,使用以下程式碼:
- var cascadeFileName = Utils.getFilePath("palm.xml");
- cascadeDetector = new CascadeClassifier();
- cascadeDetector.load(cascadeFileName);
執行檢測時,我們會進行一些必要的預處理過程:把影象轉換為灰度圖,並調整直方圖。然後,我們可以呼叫detectMultiScale函式。
- MatOfRect hands = new MatOfRect();
- Mat gray = new Mat(imgHeight, imgWidth, CvType.CV_8UC3);
- Imgproc.cvtColor(image, gray, Imgproc.COLOR_BGR2GRAY);
- Imgproc.equalizeHist(gray, gray);
- 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());
- OpenCVForUnity.CoreModule.Rect[] handsArray = hands.toArray();
- if (handsArray.Length != 0){
- //手部已經檢測到
- }
輪廓方法
基於輪廓的方法是很直接的方法,這種方法只使用了計算機視覺,沒有使用比較特別的模型。
輪廓方法基本上有二個主要步驟:
- 找到影象上符合人類皮膚顏色的區域。
- 找到符合手指的輪廓形狀。
人類皮膚顏色通常是色譜的一部分,為了找到這類顏色,我們需要把顏色形式從RGB轉換為YCrCb,並檢查每個畫素是否都在該範圍內。
- Mat YCrCb_image = new Mat();
- int Y_channel = 0;
- int Cr_channel = 1;
- int Cb_channel = 2;
- Imgproc.cvtColor(imgMat, YCrCb_image, Imgproc.COLOR_RGB2YCrCb);
- var output_mask = Mat.zeros(imgWidth, imgHeight, CvType.CV_8UC1);
- for (int i = 0; i < YCrCb_image.rows(); i++)
- {
- for (int j = 0; j < YCrCb_image.cols(); j++)
- {
- double[] p_src = YCrCb_image.get(i, j);
- 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)
- {
- output_mask.put(i, j, 255);
- }
- }
最終結果是一個遮罩,符合人類皮膚顏色的畫素是白色,剩餘部分是黑色。
在獲得這個遮罩後,我們會提取遮罩的凸殼和輪廓,並檢測“瑕疵”。“瑕疵”點表示輪廓線上遠離凸殼的點。如果“瑕疵”的角度和長度等引數符合特定標準,那麼我們就知道“瑕疵”對應著手指。
在找到足夠的“手指瑕疵”時,我們會告訴系統找到了手部,根據經驗,我們把這裡的“足夠”定義為1和4之間的數量。
下圖是瑕疵點的圖示,藍線是凸殼,綠線是輪廓。
- //在影象中找到輪廓
- List<MatOfPoint> contours = new List<MatOfPoint>();
- Imgproc.findContours(maskImage, contours, new MatOfPoint(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
- var points = new MatOfPoint(contours[index].toArray());
- var hull = new MatOfInt();
- Imgproc.convexHull(points, hull, false);
- //找到瑕疵
- var defects = new MatOfInt4();
- Imgproc.convexityDefects(points, hull, defects);
- var start_points = new MatOfPoint2f();
- var far_points = new MatOfPoint2f();
- //迴圈檢查瑕疵,瞭解它是否符合條件
- for (int i = 0; i < defects.size().height; i++)
- {
- int ind_start = (int)defects.get(i, 0)[0];
- int ind_end = (int)defects.get(i, 0)[1];
- int ind_far = (int)defects.get(i, 0)[2];
- double depth = defects.get(i, 0)[3] / 256;
- double a = Core.norm(contours[index].row(ind_start) - contours[index].row(ind_end));
- double b = Core.norm(contours[index].row(ind_far) - contours[index].row(ind_start));
- double c = Core.norm(contours[index].row(ind_far) - contours[index].row(ind_end));
- double angle = Math.Acos((b * b + c * c - a * a) / (2 * b * c)) * 180.0 / Math.PI;
- double threshFingerLength = ((double)maskImage.height()) / 8.0;
- double threshAngle = 80;
- if (angle < threshAngle && depth > threshFingerLength)
- {
- //起點
- var aa = contours[index].row(ind_start);
- start_points.push_back(contours[index].row(ind_start));
- far_points.push_back(contours[index].row(ind_far));
- }
- }
- // 檢查詢到的瑕疵是否在範圍內
- if (far_points.size().height > min_defects_count && far_points.size().height < max_defects_count)
- {
- //檢測到手部
- }
神經網路方式
在近幾年,與用於許多工的傳統解決方案相比,基於神經網路的解決方案實現了更好的效能,這在計算機視覺領域特別明顯。因為我們的任務是計算機視覺任務,所以我們自然希望使用神經網路。
實際上,目標檢測在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理解的格式。
首先要轉換模型。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資料夾。
載入模型時,使用以下程式碼:
- var modelPath = Utils.getFilePath("frozen_inference_graph.pb");
- var configPath = Utils.getFilePath("frozen_inference_graph.pbtxt");
- tfDetector = Dnn.readNetFromTensorflow(modelPath, configPath);
- 進行檢測時,使用以下程式碼:
- var blob = Dnn.blobFromImage(image, 1, new Size(300, 300), new Scalar(0, 0, 0), true, false);
- tfDetector.setInput(blob);
- Mat prob = tfDetector.forward();
- Mat newMat = prob.reshape(1, (int)prob.total() / prob.size(3));
- float maxScore = 0;
- int scoreInd = 0;
- for (int i = 0; i < newMat.rows(); i++)
- {
- var score = (float)newMat.get(i, 2)[0];
- if (score > maxScore)
- {
- maxScore = score;
- scoreInd = i;
- }
- }
- if (maxScore > thresholdScore)
- {
- // 檢測到手部
- }
我們要注意的一個引數是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
相關文章
- 擴充套件實現Unity協程的完整棧跟蹤套件Unity
- 基於行跟蹤的ROWDEPENDENCIES實現資訊變化跟蹤
- 在 React 應用程式中實現簡單的頁面檢視跟蹤器React
- Handtrack.js 開源:3行JS程式碼搞定手部動作跟蹤JS
- 利用行SCN實現表變化跟蹤
- 在Unity中實現2D光照系統Unity
- 在Unity實現遊戲命令模式Unity遊戲模式
- 如何實現Dolphinscheduler YARN Task狀態跟蹤?Yarn
- 在 Unity 多人遊戲中實現語音對話Unity遊戲
- Istio最佳實踐系列:如何實現方法級呼叫跟蹤?
- Oracle執行語句跟蹤 使用sql trace實現語句追蹤OracleSQL
- 基於OpenTelemetry實現Java微服務呼叫鏈跟蹤Java微服務
- PostgreSQL 跟蹤checkpointer出現死鎖SQL
- [zt] oracle跟蹤檔案與跟蹤事件Oracle事件
- oracle跟蹤檔案與跟蹤事件(zt)Oracle事件
- oracle跟蹤檔案和跟蹤事件(zt)Oracle事件
- 【Longkin】ASP.NET應用程式跟蹤 --- (三) 在程式碼裡訪問跟蹤資訊ASP.NET
- 使用Spring Cloud Sleuth和OpenTelemetry實現分散式跟蹤SpringCloud分散式
- 日誌系統實戰(三)-分散式跟蹤的Net實現分散式
- Unity中實現人形角色的攀爬Unity
- 在Unity中為即時戰略遊戲實現戰爭迷霧(上)Unity遊戲
- 在Unity中為即時戰略遊戲實現戰爭迷霧(下)Unity遊戲
- 在Unity中實現一個簡單的訊息管理器Unity
- 基於MeanShift的目標跟蹤演算法、實現演算法
- 淺談利用 TEB 實現的反跟蹤 (6千字)
- 敏捷專案中的跟蹤矩陣敏捷矩陣
- 在Axon框架中揭開跟蹤事件處理器的神秘面紗框架事件
- Sentry實時應用錯誤跟蹤系統在Kubernetes中私有化部署
- sqlnet跟蹤SQL
- ORACLE 跟蹤工具Oracle
- [unity3d]如何實現遊戲物件跟隨滑鼠方向移動Unity3D遊戲物件
- 能實現專案管理與BUG跟蹤系統功能的Redmine專案管理
- JVM中的本機記憶體跟蹤JVM記憶體
- 跟蹤model中屬性(值)的變更
- 關於oracle中session跟蹤的總結OracleSession
- 如何在 Git 中取消檔案的跟蹤Git
- 【Longkin】ASP.NET應用程式跟蹤---(一)跟蹤頁面ASP.NET
- 如何在Unity中實現水體互動?Unity