AR人脸跟踪

在本教程中,您将学习如何:

  • 使用AR脸部追踪功能通过TrueDepth相机追踪您的脸部。
  • 将表情符号覆盖在跟踪的脸上。
  • 根据您制作的面部表情操作表情符号。

需要的前置准备

首先要安装好Xcode,然后还需要一台带有前置TrueDepth摄像头的iPhone。也就意味着最少要使用iPhone X以上的设备,才能完成这个demo的功能

开始创建项目

启动Xcode,然后使用快捷键command + shift + n

image.png

这里我们选择 Augmented Reality App (增强现实应用),然后点击next

image.png

输入项目名字,然后选择 Swift 语言,Content Technology 选择 SceneKit,然后点击next,此时将在您选择的置顶位置创建好项目

项目中的文件的用处

创建好项目之后,默认会有一个art.scnassets的文件夹,在这个文件夹下面有两个文件,正如图片中的,一个是ship.scn, 一个是texture.png,scn文件是3D模型的一种,如果没有texture.png这个图片,那么将显示这样的效果

image.png

texture.png被附着到ship.scn这个3d模型上之后,就成了下面的这个样子,变的更好看了,蒙了一层皮,我们称之为纹理,实际上不仅仅能把图片当成ship.scn的”皮“,UIColor,CALayer,甚至是AVPlayer,都可以,暂时不做过多探究

image.png

说了这么多,感觉有点枯燥,还是来点代码吧

代码展示

现在到了最重要的环节,代码展示环节


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
        
    }
}


仅仅以上几十行代码,我们将能体验到一个非常牛叉的AR飞船的效果,虽然不知道是什么机型,但是不明觉厉,

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

是的,就这样,第一个AR应用就完成了,但是仍然不知道是怎么回事啊,那么来分析分析是怎么实现的呢

首先,需要一个来显示AR渲染效果的视图ARSCNView,

class ARSCNView : SCNView

SCNView是用来干嘛的呢。用于显示3D SceneKit内容的视图
在macOS中,SCNView是NSView的子类。 在iOS和tvOS中,SCNView是UIView的子类。 作为这两种操作系统的视图层次结构的一部分,SCNView对象在应用程序的用户界面中为SceneKit内容提供了一个位置。 您可以使用它的init(frame:options :)方法或将其添加到nib文件或情节提要中来创建一个SceneKit视图。 要为SceneKit视图提供内容,请将SCNScene对象分配给它的scene属性。

有关使用SceneKit视图的其他重要方法和属性,请参见SCNSceneRenderer协议。 (您还可以使用SCNRenderer类将SceneKit内容渲染到任意Metal命令队列或OpenGL上下文中,或者使用SCNLayer类渲染到macOS上的Core Animation层中。SCNSceneRenderer协议定义了这三个SceneKit渲染类共有的功能。)

sceneView.delegate = self

设置ViewController为sceneView.的代理,那么就可以在ARSCNViewDelegate的代理回调中处理业务上的需求,

sceneView.showsStatistics = true

设置是否显示分析数据,方便分析和调试

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

这个的作用就是加载我们项目中的3d模型资源,从而将3d模型在sceneView中展示出来

SCNScene 是节点层次结构和全局属性的容器,它们共同构成了可显示的3D场景。

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

将我们刚刚创建好的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)
    }

在视图将要展示的时候,创建一个ARWorldTrackingConfiguration对象
ARWorldTrackingConfiguration的作用是
一种配置,用于监视iOS设备的位置和方向,同时使您能够扩展用户面前的环境。然后 sceneView.session.run(configuration),这样就能将飞船模型展示在相机拍到的现实世界中了。

人脸检测能力

通过上面的简单分析,我们并不需要做很多的操作,写很多的代码,就能实现一个简单的AR应用,暂且将上面的分析作为对AR的一个认识,下面将实现检测到人脸,并且在人脸上贴上一些有趣的emoji表情

我们要改变的代码并不是很多


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

首先在viewDidLoad方法中加入这段代码,这段代码是来检测您当前的设备是否支持人脸识别,不支持的设备将无法完成本教程的余下功能。

您已经看过ARFaceTrackingConfiguration,它用于配置设备以使用TrueDepth摄像头跟踪您的脸部。酷啊。

但是您还需要了解有关面部跟踪的哪些信息?

您即将使用的三个非常重要的类是ARFaceAnchor,ARFaceGeometry和ARSCNFaceGeometry。

ARFaceAnchor继承自ARAnchor。如果您以前使用ARKit做过任何事情,您就会知道ARAnchors是使它如此强大和简单的原因。它们是ARKit跟踪的现实世界中的位置,移动手机时这些位置不会移动。 ARFaceAnchors还包括有关面部的信息,例如拓扑和表情。

ARFaceGeometry听起来很像。这是一张包含顶点和TextureCoordinates的3D描述。

ARSCNFaceGeometry使用来自ARFaceGeometry的数据来创建SCNGeometry,该SCNGeometry可用于创建SceneKit节点-基本上就是您在屏幕上看到的内容。

好,够了是时候使用这些类了。回到编码!

添加 Mesh Mask

从表面上看,您似乎只打开了前置摄像头。 但是,您看不到您的iPhone已经在跟踪您的脸。 令人毛骨悚然的iPhone。

看到iPhone跟踪的内容不是很好吗? 真是巧合,因为这正是您接下来要做的!

在ViewController类定义的大括号后添加以下代码:

// 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实现ARSCNViewDelegate协议。
  • 从协议定义renderer(_:nodeFor :)方法。
  • 确保用于渲染的金属设备不为零。
  • 创建要由“金属”设备渲染的面几何。
  • 根据面部几何形状创建一个SceneKit节点。
  • 将节点材质的填充模式设置为仅线。
  • 返回节点。

在运行代码之前,确保在viewDidLoad中设置了代理

sceneView.delegate = self

此时运行App你将能够在自己的手机里自己的脸上有层遮罩网格。

更新 Mesh Mask

您是否注意到网格遮罩有点……是静态的? 当然,当您左右移动头部时,它会跟踪您的面部位置并随之移动,但是眨眼或张开嘴会发生什么呢? 没有。

真令人失望。

幸运的是,这很容易解决。 您只需要添加另一个ARSCNViewDelegate方法!

在您的ARSCNViewDelegate扩展的末尾,添加以下方法:


// 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 :)协议方法的didUpdate版本。
  • 确保要更新的锚是ARFaceAnchor,并且节点的几何是ARSCNFaceGeometry。
  • 使用ARFaceAnchor的ARFaceGeometry更新ARSCNFaceGeometry
    现在,在构建和运行时,您应该看到网格蒙版表格并进行更改以匹配您的面部表情。

再次运行程序,你会发现网格遮罩跟着脸部在运动了,很神奇。

添加Emoji表情

下面我们定义了一些有趣的表情

    let noseOptions = ["👃", "🐽", "💧", " "]
    let eyeOptions = ["👁", "🌕", "🌟", "🔥", "⚽️", "🔎", " "]
    let mouthOptions = ["👄", "👅", "❤️", " "]
    let hatOptions = ["🎓", "🎩", "🧢", "⛑", "👒", " "]
    let features = ["nose", "leftEye", "rightEye", "mouth", "hat"]
    let featureIndices = [[9], [1064], [42], [24, 25], [20]]

以上的表情分别是

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

下面我们定义一个类,用来将String转成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
  }
}

然后定义一个EmojiNode 类,这个类是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
    }
  }
}

这个类的作用是根据指定的字符串,创建一个图片节点,并且能够更新位置

在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
          }
        }
      }

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

在代理方法中添加嘴,眼睛,鼻子,帽子节点,并在锚点的更新方法中更新Emoji表情节点

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)
  }
}

看看最终的效果

到目前位置,所有主要的代码都在上面部分,最后运行项目,看看最终的效果

19_Full_App_Weirdness-1.gif

先讲到这里了,感兴趣的同学们,可以下载代码来调试一下,本教程参考资料来自于 《ARKit by Tutorials》这本书

demo下载地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,458评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,030评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,879评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,278评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,296评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,019评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,633评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,541评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,068评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,181评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,318评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,991评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,670评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,183评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,302评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,655评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,327评论 2 358