1. 程式人生 > >Unity AR Foundation 和 CoreML: 實現手部的檢測和追蹤

Unity AR Foundation 和 CoreML: 實現手部的檢測和追蹤

0x00 前言

Unity的AR Foundation通過上層抽象,對ARKit和ARCore這些底層介面進行了封裝,從而實現了AR專案的跨平臺開發能力。

而蘋果的CoreML是一個可以用來將機器學習模型與iOS平臺上的app進行整合的框架。

本文以及本文結尾處的demo工程,將介紹和演示如何使Unity的AR Foundation與蘋果的CoreML一同工作,以實現使用我們的手來和虛擬物體進行互動的功能。

Unity AR Foundation手部檢測

 

本文參考了Gil Nakache的文章,並且所使用的機器學習模型也來自他的文章。在他的那篇文章中,他描述瞭如何使用Swift在iOS原生平臺上實現類似的功能。

Version

Unity Version: 2018.3.13f1

Xcode Version: 10.2.1

The ARFoundation Plugin: 1.5.0-preview.5

iPhone 7: 12.3.1

 

0x01 實現

匯入 AR Foundation Plugin

為了方便,我使用了本地pacakge匯入的形式。這種實現方式十分簡單,只需要修改工程目錄下Package資料夾內的manifest.json檔案,在manifest.json檔案中新增本地package即可。

"com.unity.xr.arfoundation": "file:../ARPackages/com.unity.xr.arfoundation", 
"com.unity.xr.arkit": "file:../ARPackages/com.unity.xr.arkit

匯入AR Foundation Package之後,我們就可以在場景中建立一些相關的元件了,比如AR Session、AR Session Origin等等。

之後在我們的指令碼中,監聽frameReceived事件來獲取每一幀的資料。

    if (m_CameraManager != null)
    {
        m_CameraManager.frameReceived += OnCameraFrameReceived;
    }

使用Swift語言建立一個Unity外掛

為了使C#語言可以和Swift語言進行互動,我們需要先建立一個Objective-C檔案作為橋接。這種方式就是,C#通過[DllImport("__Internal")]來呼叫一個Objective-C的方法。之後,Objective-C再通過@objc來呼叫Swift。引入UnityInterface.h之後,Swift可以呼叫UnitySendMessage方法來向C#傳送資料。

這裡有一個示例工程,演示瞭如何為Unity建立一個使用Swift的原生外掛,並且在Unity中打印出“Hello, I’m Swift”。

本文所使用的Unity-ARFoundation-HandDetection工程,它的plugins資料夾的目錄結構如下:

但是,需要注意的是,Unity直接匯出的Xcode工程是沒有指定Swift版本的。

因此,我們需要手動指定一個版本,或者建立一個Unity的指令碼來自動設定Swift的版本。

匯入 mlmodel

將HandModel新增到我們的Xcode工程中,之後它會自動生成一個Objective-C model類。但是我希望得到一個Swift的類,因此我們可以在Build Settings/CoreML Model Compiler - Code Generation Language這裡將選項從Auto修改為Swift。

之後,我們會獲得一個叫做HandModel的自動生成的Swift類。

當然,如果你不想總是手動新增,同樣也可以選擇在Unity中建立一個build post processing指令碼來自動新增機器學習模型。

如何從AR Foundation中獲取ARFrame Ptr

完成了以上步驟之後,基本的互動框架就已經成型了。接下來,我們就需要使用CoreML來實現手部的檢測和追蹤的具體功能了。

@objc func startDetection(buffer: CVPixelBuffer) -> Bool {
    //TODO
    self.retainedBuffer = buffer
    let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: self.retainedBuffer!, orientation: .right)
    
    visionQueue.async {
        do {
            defer { self.retainedBuffer = nil }
            try imageRequestHandler.perform([self.predictionRequest])
        } catch {
            fatalError("Perform Failed:\"\(error)\"")
        }
    }
    
    return true
}

在Swift中,我們需要一個CVPixelBuffer來建立VNImageRequestHandler以執行手部檢測。通常我們需要從ARFrame中來獲取它。

CVPixelBufferRef buffer = frame.capturedImage;

因此,下一個問題就是如何從Unity的AR Foundation的C#指令碼中獲取來自ARKit的ARFrame指標,並且將其傳遞給使用Objective-C和Swift語言的Hand Detection外掛。

在AR Foundation中,我們可以從XRCameraFrame中獲取nativePtr,它指向一個ARKit的結構,如下所示:

typedef struct UnityXRNativeFrame_1
{
    int version;
    void* framePtr;
} UnityXRNativeFrame_1;

並且這個framePtr指向了最新的ARFrame

具體來說,我們可以呼叫定義在XRCamera​Subsystem的TryGetLatestFrame方法來獲取一個XRCameraFrame例項。

cameraManager.subsystem.TryGetLatestFrame(cameraParams, out frame)

之後將nativePtr從C#傳遞給Objective-C。

m_HandDetector.StartDetect(frame.nativePtr);

在Objective-C這邊,我們會獲得一個UnityXRNativeFrame_1指標並且我們能從其中獲取ARFrame指標。

    UnityXRNativeFrame_1* unityXRFrame = (UnityXRNativeFrame_1*) ptr;
    ARFrame* frame = (__bridge ARFrame*)unityXRFrame->framePtr;
    
    CVPixelBufferRef buffer = frame.capturedImage

一旦獲取了ARFrame,接下來就來到了iOS開發的領域。建立一個VNImageRequestHandler物件並且開始執行手部檢測。一旦檢測完成,detectionCompleteHandler回撥會被呼叫並且會通過UnitySendMessage將檢測的結果傳遞給Unity。

private func detectionCompleteHandler(request: VNRequest, error: Error?) {
    
    DispatchQueue.main.async {
        
        if(error != nil) {
            UnitySendMessage(self.callbackTarget, "OnHandDetecedFromNative", "")
            fatalError("error\(error)")
        }
        
        guard let observation = self.predictionRequest.results?.first as? VNPixelBufferObservation else {
            UnitySendMessage(self.callbackTarget, "OnHandDetecedFromNative", "")
            fatalError("Unexpected result type from VNCoreMLRequest")
        }
        
        let outBuffer = observation.pixelBuffer
        
        guard let point = outBuffer.searchTopPoint() else{
            UnitySendMessage(self.callbackTarget, "OnHandDetecedFromNative", "")
            return
        }
        
        UnitySendMessage(self.callbackTarget, "OnHandDetecedFromNative", "\(point.x),\(point.y)")
    }
}

之後我們會獲取在viewport空間的position資料。viewport空間是相對於相機標準化的。 viewport的左下角是(0,0); 右上角是(1,1)。

一旦我們獲取了viewport空間的位置,就可以通過Unity的ViewportToWorldPoint方法將它從viewport空間轉換到world空間。傳遞給該方法的向量引數中的x、y來自Hand Detection的結果,z值則是距離相機的距離。

   var handPos = new Vector3();
   handPos.x = pos.x;
   handPos.y = 1 - pos.y;
   handPos.z = 4;//m_Cam.nearClipPlane;
   var handWorldPos = m_Cam.ViewportToWorldPoint(handPos);

我們可以在Unity中使用這個世界座標來建立新的Object,或者是將已有的Object移動到這個世界座標。換句話說,這個Object的位置會根據手的位置而改變。

Post Process Build

正如上文說過的,我們可以在Unity中寫一個C#指令碼來自動設定生成的Xcode工程中的一些屬性。例如,我們可以設定Xcode工程中Build Setting中的Swift Version屬性。我們甚至還可以將機器學習模型新增到Build Phases中,比如新增到Compile Sources Phase。這裡我們會使用定義在UnityEditor.iOS.Xcode名稱空間中的PBXProject類。PBXProject類提供了很多有用的方法,例如AddBuildPropertySetBuildPropertyAddSourcesBuildPhase

[PostProcessBuild]
public static void OnPostProcessBuild(BuildTarget buildTarget, string path)
{
    if(buildTarget != BuildTarget.iOS)
    {
        return;
    }

    string projPath = path + "/Unity-iPhone.xcodeproj/project.pbxproj";
    
    var proj = new PBXProject();
    proj.ReadFromFile(projPath);
    var targetGUID = proj.TargetGuidByName("Unity-iPhone");

    //set xcode proj properties
    proj.AddBuildProperty(targetGUID, "SWIFT_VERSION", "4.0");
    proj.SetBuildProperty(targetGUID, "SWIFT_OBJC_BRIDGING_HEADER", "Libraries/Plugins/iOS/HandDetector/Native/HandDetector.h");
    proj.SetBuildProperty(targetGUID, "SWIFT_OBJC_INTERFACE_HEADER_NAME","HandDetector-Swift.h");
    proj.SetBuildProperty(targetGUID, "COREML_CODEGEN_LANGUAGE", "Swift");
    
    
    //add handmodel to xcode proj build phase.
    var buildPhaseGUID = proj.AddSourcesBuildPhase(targetGUID);
    var handModelPath = Application.dataPath + "/../CoreML/HandModel.mlmodel";
    var fileGUID = proj.AddFile(handModelPath, "/HandModel.mlmodel");
    proj.AddFileToBuildSection(targetGUID, buildPhaseGUID, fileGUID);
    
    proj.WriteToFile(projPath);

}

0x02 結論

使用Unity中的AR Foundation和CoreML,我們可以讓Unity Chan站在我們的手指上。

本文簡單描述了整合CoreML和AR Foundation的過程。我相信大家可以使用它們作出更有趣的內容。

這裡是文中所使用的demo工程。

 

https://github.com/chenjd/Unity-ARFoundation-HandDetection

 

 

Useful Links

https://heartbeat.fritz.ai/hand-detection-with-core-ml-and-arkit-f4c8da98e88e

https://medium.com/@kevinhuyskens/implementing-swift-in-unity-53e0b668f895

http://chenjd.xyz/2019/07/22/Unity-ARFoundation-CoreML/

&n