AR人臉跟蹤

在本教程中潜腻,您將學(xué)習(xí)如何:

  • 使用AR臉部追蹤功能通過TrueDepth相機(jī)追蹤您的臉部融涣。
  • 將表情符號(hào)覆蓋在跟蹤的臉上威鹿。
  • 根據(jù)您制作的面部表情操作表情符號(hào)轨香。

需要的前置準(zhǔn)備

首先要安裝好Xcode臂容,然后還需要一臺(tái)帶有前置TrueDepth攝像頭的iPhone。也就意味著最少要使用iPhone X以上的設(shè)備简逮,才能完成這個(gè)demo的功能

開始創(chuàng)建項(xiàng)目

啟動(dòng)Xcode散庶,然后使用快捷鍵command + shift + n

image.png

這里我們選擇 Augmented Reality App (增強(qiáng)現(xiàn)實(shí)應(yīng)用)悲龟,然后點(diǎn)擊next

image.png

輸入項(xiàng)目名字须教,然后選擇 Swift 語言没卸,Content Technology 選擇 SceneKit秒旋,然后點(diǎn)擊next迁筛,此時(shí)將在您選擇的置頂位置創(chuàng)建好項(xiàng)目

項(xiàng)目中的文件的用處

創(chuàng)建好項(xiàng)目之后细卧,默認(rèn)會(huì)有一個(gè)art.scnassets的文件夾贪庙,在這個(gè)文件夾下面有兩個(gè)文件止邮,正如圖片中的,一個(gè)是ship.scn, 一個(gè)是texture.png屈扎,scn文件是3D模型的一種鹰晨,如果沒有texture.png這個(gè)圖片模蜡,那么將顯示這樣的效果

image.png

texture.png被附著到ship.scn這個(gè)3d模型上之后哩牍,就成了下面的這個(gè)樣子膝昆,變的更好看了荚孵,蒙了一層皮,我們稱之為紋理骄呼,實(shí)際上不僅僅能把圖片當(dāng)成ship.scn的”皮“蜓萄,UIColor嫉沽,CALayer俏竞,甚至是AVPlayer魂毁,都可以席楚,暫時(shí)不做過多探究

image.png

說了這么多烦秩,感覺有點(diǎn)枯燥闻镶,還是來點(diǎn)代碼吧

代碼展示

現(xiàn)在到了最重要的環(huán)節(jié)铆农,代碼展示環(huán)節(jié)


import UIKit
import SceneKit
import ARKit

class ViewController: UIViewController, ARSCNViewDelegate {

    @IBOutlet var sceneView: ARSCNView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Set the view's delegate
        sceneView.delegate = self
        
        // Show statistics such as fps and timing information
        sceneView.showsStatistics = true
        
        // Create a new scene
        let scene = SCNScene(named: "art.scnassets/ship.scn")!
        
        // Set the scene to the view
        sceneView.scene = scene
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        // Create a session configuration
        let configuration = ARWorldTrackingConfiguration()

        // Run the view's session
        sceneView.session.run(configuration)
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        // Pause the view's session
        sceneView.session.pause()
    }

    // MARK: - ARSCNViewDelegate
    
/*
    // Override to create and configure nodes for anchors added to the view's session.
    func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
        let node = SCNNode()
     
        return node
    }
*/
    
    func session(_ session: ARSession, didFailWithError error: Error) {
        // Present an error message to the user
        
    }
    
    func sessionWasInterrupted(_ session: ARSession) {
        // Inform the user that the session has been interrupted, for example, by presenting an overlay
        
    }
    
    func sessionInterruptionEnded(_ session: ARSession) {
        // Reset tracking and/or remove existing anchors if consistent tracking is required
        
    }
}


僅僅以上幾十行代碼猴凹,我們將能體驗(yàn)到一個(gè)非常牛叉的AR飛船的效果郊霎,雖然不知道是什么機(jī)型书劝,但是不明覺厲土至,

middle_img_674fba72-e36e-4ad6-9fa7-788a47ce4a5g.jpg

是的,就這樣陶因,第一個(gè)AR應(yīng)用就完成了骡苞,但是仍然不知道是怎么回事啊,那么來分析分析是怎么實(shí)現(xiàn)的呢

首先楷扬,需要一個(gè)來顯示AR渲染效果的視圖ARSCNView解幽,

class ARSCNView : SCNView

SCNView是用來干嘛的呢。用于顯示3D SceneKit內(nèi)容的視圖
在macOS中烘苹,SCNView是NSView的子類躲株。 在iOS和tvOS中,SCNView是UIView的子類螟加。 作為這兩種操作系統(tǒng)的視圖層次結(jié)構(gòu)的一部分,SCNView對(duì)象在應(yīng)用程序的用戶界面中為SceneKit內(nèi)容提供了一個(gè)位置捆探。 您可以使用它的init(frame:options :)方法或?qū)⑵涮砑拥絥ib文件或情節(jié)提要中來創(chuàng)建一個(gè)SceneKit視圖然爆。 要為SceneKit視圖提供內(nèi)容,請將SCNScene對(duì)象分配給它的scene屬性黍图。

有關(guān)使用SceneKit視圖的其他重要方法和屬性曾雕,請參見SCNSceneRenderer協(xié)議。 (您還可以使用SCNRenderer類將SceneKit內(nèi)容渲染到任意Metal命令隊(duì)列或OpenGL上下文中助被,或者使用SCNLayer類渲染到macOS上的Core Animation層中剖张。SCNSceneRenderer協(xié)議定義了這三個(gè)SceneKit渲染類共有的功能。)

sceneView.delegate = self

設(shè)置ViewController為sceneView.的代理揩环,那么就可以在ARSCNViewDelegate的代理回調(diào)中處理業(yè)務(wù)上的需求搔弄,

sceneView.showsStatistics = true

設(shè)置是否顯示分析數(shù)據(jù),方便分析和調(diào)試

 // Create a new scene
 let scene = SCNScene(named: "art.scnassets/ship.scn")!

這個(gè)的作用就是加載我們項(xiàng)目中的3d模型資源丰滑,從而將3d模型在sceneView中展示出來

SCNScene 是節(jié)點(diǎn)層次結(jié)構(gòu)和全局屬性的容器顾犹,它們共同構(gòu)成了可顯示的3D場景。

// Set the scene to the view
sceneView.scene = scene

將我們剛剛創(chuàng)建好的scene 賦值給sceneView的scene

override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        // Create a session configuration
        let configuration = ARWorldTrackingConfiguration()

        // Run the view's session
        sceneView.session.run(configuration)
    }

在視圖將要展示的時(shí)候,創(chuàng)建一個(gè)ARWorldTrackingConfiguration對(duì)象
ARWorldTrackingConfiguration的作用是
一種配置炫刷,用于監(jiān)視iOS設(shè)備的位置和方向擎宝,同時(shí)使您能夠擴(kuò)展用戶面前的環(huán)境。然后 sceneView.session.run(configuration)浑玛,這樣就能將飛船模型展示在相機(jī)拍到的現(xiàn)實(shí)世界中了绍申。

人臉檢測能力

通過上面的簡單分析,我們并不需要做很多的操作顾彰,寫很多的代碼极阅,就能實(shí)現(xiàn)一個(gè)簡單的AR應(yīng)用,暫且將上面的分析作為對(duì)AR的一個(gè)認(rèn)識(shí)拘央,下面將實(shí)現(xiàn)檢測到人臉涂屁,并且在人臉上貼上一些有趣的emoji表情

我們要改變的代碼并不是很多


guard ARFaceTrackingConfiguration.isSupported else {
  fatalError("Face tracking is not supported on this device")
}

首先在viewDidLoad方法中加入這段代碼,這段代碼是來檢測您當(dāng)前的設(shè)備是否支持人臉識(shí)別灰伟,不支持的設(shè)備將無法完成本教程的余下功能拆又。

您已經(jīng)看過ARFaceTrackingConfiguration,它用于配置設(shè)備以使用TrueDepth攝像頭跟蹤您的臉部栏账√澹酷啊。

但是您還需要了解有關(guān)面部跟蹤的哪些信息挡爵?

您即將使用的三個(gè)非常重要的類是ARFaceAnchor竖般,ARFaceGeometry和ARSCNFaceGeometry。

ARFaceAnchor繼承自ARAnchor茶鹃。如果您以前使用ARKit做過任何事情涣雕,您就會(huì)知道ARAnchors是使它如此強(qiáng)大和簡單的原因。它們是ARKit跟蹤的現(xiàn)實(shí)世界中的位置闭翩,移動(dòng)手機(jī)時(shí)這些位置不會(huì)移動(dòng)挣郭。 ARFaceAnchors還包括有關(guān)面部的信息,例如拓?fù)浜捅砬椤?/p>

ARFaceGeometry聽起來很像疗韵。這是一張包含頂點(diǎn)和TextureCoordinates的3D描述兑障。

ARSCNFaceGeometry使用來自ARFaceGeometry的數(shù)據(jù)來創(chuàng)建SCNGeometry,該SCNGeometry可用于創(chuàng)建SceneKit節(jié)點(diǎn)-基本上就是您在屏幕上看到的內(nèi)容蕉汪。

好流译,夠了是時(shí)候使用這些類了≌甙蹋回到編碼福澡!

添加 Mesh Mask

從表面上看,您似乎只打開了前置攝像頭驹马。 但是竞漾,您看不到您的iPhone已經(jīng)在跟蹤您的臉眯搭。 令人毛骨悚然的iPhone。

看到iPhone跟蹤的內(nèi)容不是很好嗎业岁? 真是巧合,因?yàn)檫@正是您接下來要做的寇蚊!

在ViewController類定義的大括號(hào)后添加以下代碼:

// 1
extension ViewController: ARSCNViewDelegate {
  // 2
  func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
    
    // 3
    guard let device = sceneView.device else {
      return nil
    }
    
    // 4
    let faceGeometry = ARSCNFaceGeometry(device: device)
    
    // 5
    let node = SCNNode(geometry: faceGeometry)
    
    // 6
    node.geometry?.firstMaterial?.fillMode = .lines
    
    // 7
    return node
  }
}

上面的代碼做了這幾件事

  • 聲明ViewController實(shí)現(xiàn)ARSCNViewDelegate協(xié)議笔时。
  • 從協(xié)議定義renderer(_:nodeFor :)方法。
  • 確保用于渲染的金屬設(shè)備不為零仗岸。
  • 創(chuàng)建要由“金屬”設(shè)備渲染的面幾何允耿。
  • 根據(jù)面部幾何形狀創(chuàng)建一個(gè)SceneKit節(jié)點(diǎn)。
  • 將節(jié)點(diǎn)材質(zhì)的填充模式設(shè)置為僅線扒怖。
  • 返回節(jié)點(diǎn)较锡。

在運(yùn)行代碼之前,確保在viewDidLoad中設(shè)置了代理

sceneView.delegate = self

此時(shí)運(yùn)行App你將能夠在自己的手機(jī)里自己的臉上有層遮罩網(wǎng)格盗痒。

更新 Mesh Mask

您是否注意到網(wǎng)格遮罩有點(diǎn)……是靜態(tài)的蚂蕴? 當(dāng)然,當(dāng)您左右移動(dòng)頭部時(shí)俯邓,它會(huì)跟蹤您的面部位置并隨之移動(dòng)骡楼,但是眨眼或張開嘴會(huì)發(fā)生什么呢? 沒有稽鞭。

真令人失望鸟整。

幸運(yùn)的是,這很容易解決朦蕴。 您只需要添加另一個(gè)ARSCNViewDelegate方法篮条!

在您的ARSCNViewDelegate擴(kuò)展的末尾,添加以下方法:


// 1
func renderer(
  _ renderer: SCNSceneRenderer, 
  didUpdate node: SCNNode, 
  for anchor: ARAnchor) {
   
  // 2
  guard let faceAnchor = anchor as? ARFaceAnchor,
    let faceGeometry = node.geometry as? ARSCNFaceGeometry else {
      return
  }
    
  // 3
  faceGeometry.update(from: faceAnchor.geometry)
}

上面的方法做了這幾件事情

  • 定義renderer(_:didUpdate:for :)協(xié)議方法的didUpdate版本吩抓。
  • 確保要更新的錨是ARFaceAnchor涉茧,并且節(jié)點(diǎn)的幾何是ARSCNFaceGeometry。
  • 使用ARFaceAnchor的ARFaceGeometry更新ARSCNFaceGeometry
    現(xiàn)在琴拧,在構(gòu)建和運(yùn)行時(shí)降瞳,您應(yīng)該看到網(wǎng)格蒙版表格并進(jìn)行更改以匹配您的面部表情。

再次運(yùn)行程序蚓胸,你會(huì)發(fā)現(xiàn)網(wǎng)格遮罩跟著臉部在運(yùn)動(dòng)了挣饥,很神奇。

添加Emoji表情

下面我們定義了一些有趣的表情

    let noseOptions = ["??", "??", "??", " "]
    let eyeOptions = ["??", "??", "??", "??", "??", "??", " "]
    let mouthOptions = ["??", "??", "??", " "]
    let hatOptions = ["??", "??", "??", "?", "??", " "]
    let features = ["nose", "leftEye", "rightEye", "mouth", "hat"]
    let featureIndices = [[9], [1064], [42], [24, 25], [20]]

以上的表情分別是

  • 鼻子上的
  • 眼睛上的
  • 嘴上的
  • 頭上的

下面我們定義一個(gè)類沛膳,用來將String轉(zhuǎn)成UIImage

extension String {
  
  func image() -> UIImage? {
    
    let size = CGSize(width: 20, height: 22)
    
    UIGraphicsBeginImageContextWithOptions(size, false, 0)
    UIColor.clear.set()
    
    let rect = CGRect(origin: .zero, size: size)
    UIRectFill(CGRect(origin: .zero, size: size))
    
    (self as AnyObject).draw(in: rect, withAttributes: [.font: UIFont.systemFont(ofSize: 15)])
    
    let image = UIGraphicsGetImageFromCurrentImageContext()
    
    UIGraphicsEndImageContext()
    
    return image
  }
}

然后定義一個(gè)EmojiNode 類扔枫,這個(gè)類是SCNNode的子類,

import SceneKit

class EmojiNode: SCNNode {
  
  var options: [String]
  var index = 0
  
  init(with options: [String], width: CGFloat = 0.06, height: CGFloat = 0.06) {
    self.options = options
    
    super.init()
    
    let plane = SCNPlane(width: width, height: height)
    plane.firstMaterial?.diffuse.contents = (options.first ?? " ").image()
    plane.firstMaterial?.isDoubleSided = true
    
    geometry = plane
  }
  
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}

// MARK: - Custom functions

extension EmojiNode {
  
  func updatePosition(for vectors: [vector_float3]) {
    let newPos = vectors.reduce(vector_float3(), +) / Float(vectors.count)
    position = SCNVector3(newPos)
  }
  
  func next() {
    index = (index + 1) % options.count
    
    if let plane = geometry as? SCNPlane {
      plane.firstMaterial?.diffuse.contents = options[index].image()
      plane.firstMaterial?.isDoubleSided = true
    }
  }
}

這個(gè)類的作用是根據(jù)指定的字符串锹安,創(chuàng)建一個(gè)圖片節(jié)點(diǎn)短荐,并且能夠更新位置

在ViewController中添加如下方法


func updateFeatures(for node: SCNNode, using anchor: ARFaceAnchor) {
        for (feature, indices) in zip(features, featureIndices) {
          let child = node.childNode(withName: feature, recursively: false) as? EmojiNode
          let vertices = indices.map { anchor.geometry.vertices[$0] }
          child?.updatePosition(for: vertices)
          
          switch feature {
          case "leftEye":
            let scaleX = child?.scale.x ?? 1.0
            let eyeBlinkValue = anchor.blendShapes[.eyeBlinkLeft]?.floatValue ?? 0.0
            child?.scale = SCNVector3(scaleX, 1.0 - eyeBlinkValue, 1.0)
          case "rightEye":
            let scaleX = child?.scale.x ?? 1.0
            let eyeBlinkValue = anchor.blendShapes[.eyeBlinkRight]?.floatValue ?? 0.0
            child?.scale = SCNVector3(scaleX, 1.0 - eyeBlinkValue, 1.0)
          case "mouth":
            let jawOpenValue = anchor.blendShapes[.jawOpen]?.floatValue ?? 0.2
            child?.scale = SCNVector3(1.0, 0.8 + jawOpenValue, 1.0)
          default:
            break
          }
        }
      }

此方法的作用是更新面部表情

在代理方法中添加嘴倚舀,眼睛,鼻子忍宋,帽子節(jié)點(diǎn)痕貌,并在錨點(diǎn)的更新方法中更新Emoji表情節(jié)點(diǎn)

extension ViewController: ARSCNViewDelegate {
  
  func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
    guard let faceAnchor = anchor as? ARFaceAnchor,
          let device = sceneView.device else { return nil }
    let faceGeometry = ARSCNFaceGeometry(device: device)
    let node = SCNNode(geometry: faceGeometry)
    node.geometry?.firstMaterial?.fillMode = .lines
    node.geometry?.firstMaterial?.transparency = 0.0
    
    let noseNode = EmojiNode(with: noseOptions)
    noseNode.name = "nose"
    node.addChildNode(noseNode)
    
    let leftEyeNode = EmojiNode(with: eyeOptions)
    leftEyeNode.name = "leftEye"
    leftEyeNode.rotation = SCNVector4(0, 1, 0, GLKMathDegreesToRadians(180.0))
    node.addChildNode(leftEyeNode)
    
    let rightEyeNode = EmojiNode(with: eyeOptions)
    rightEyeNode.name = "rightEye"
    node.addChildNode(rightEyeNode)
    
    let mouthNode = EmojiNode(with: mouthOptions)
    mouthNode.name = "mouth"
    node.addChildNode(mouthNode)
    
    let hatNode = EmojiNode(with: hatOptions)
    hatNode.name = "hat"
    node.addChildNode(hatNode)
    
    updateFeatures(for: node, using: faceAnchor)
    return node
  }
  
  func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
    guard let faceAnchor = anchor as? ARFaceAnchor, let faceGeometry = node.geometry as? ARSCNFaceGeometry else { return }
    
    faceGeometry.update(from: faceAnchor.geometry)
    updateFeatures(for: node, using: faceAnchor)
  }
}

看看最終的效果

到目前位置,所有主要的代碼都在上面部分糠排,最后運(yùn)行項(xiàng)目舵稠,看看最終的效果

19_Full_App_Weirdness-1.gif

先講到這里了,感興趣的同學(xué)們入宦,可以下載代碼來調(diào)試一下哺徊,本教程參考資料來自于 《ARKit by Tutorials》這本書

demo下載地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市乾闰,隨后出現(xiàn)的幾起案子落追,更是在濱河造成了極大的恐慌,老刑警劉巖涯肩,帶你破解...
    沈念sama閱讀 218,386評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件轿钠,死亡現(xiàn)場離奇詭異,居然都是意外死亡宽菜,警方通過查閱死者的電腦和手機(jī)谣膳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來铅乡,“玉大人继谚,你說我怎么就攤上這事≌笮遥” “怎么了花履?”我有些...
    開封第一講書人閱讀 164,704評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長挚赊。 經(jīng)常有香客問我诡壁,道長,這世上最難降的妖魔是什么荠割? 我笑而不...
    開封第一講書人閱讀 58,702評(píng)論 1 294
  • 正文 為了忘掉前任妹卿,我火速辦了婚禮,結(jié)果婚禮上蔑鹦,老公的妹妹穿的比我還像新娘夺克。我一直安慰自己,他們只是感情好嚎朽,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評(píng)論 6 392
  • 文/花漫 我一把揭開白布铺纽。 她就那樣靜靜地躺著,像睡著了一般哟忍。 火紅的嫁衣襯著肌膚如雪狡门。 梳的紋絲不亂的頭發(fā)上陷寝,一...
    開封第一講書人閱讀 51,573評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音其馏,去河邊找鬼凤跑。 笑死,一個(gè)胖子當(dāng)著我的面吹牛叛复,可吹牛的內(nèi)容都是我干的饶火。 我是一名探鬼主播,決...
    沈念sama閱讀 40,314評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼致扯,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了当辐?” 一聲冷哼從身側(cè)響起抖僵,我...
    開封第一講書人閱讀 39,230評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎缘揪,沒想到半個(gè)月后耍群,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,680評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡找筝,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評(píng)論 3 336
  • 正文 我和宋清朗相戀三年蹈垢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片袖裕。...
    茶點(diǎn)故事閱讀 39,991評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡曹抬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出急鳄,到底是詐尸還是另有隱情谤民,我是刑警寧澤,帶...
    沈念sama閱讀 35,706評(píng)論 5 346
  • 正文 年R本政府宣布疾宏,位于F島的核電站张足,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏坎藐。R本人自食惡果不足惜为牍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望岩馍。 院中可真熱鬧碉咆,春花似錦、人聲如沸兼雄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赦肋。三九已至块攒,卻和暖如春励稳,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背囱井。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評(píng)論 1 270
  • 我被黑心中介騙來泰國打工驹尼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人庞呕。 一個(gè)月前我還...
    沈念sama閱讀 48,158評(píng)論 3 370
  • 正文 我出身青樓新翎,卻偏偏與公主長得像,于是被迫代替她去往敵國和親住练。 傳聞我的和親對(duì)象是個(gè)殘疾皇子地啰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評(píng)論 2 355