Swift ARKit 臉部特效

簡介

ARKit 讚讚

基本設置

新增ARSCNView

override func viewDidLoad() {
    super.viewDidLoad()
    sceneView = ARSCNView()
    sceneView.frame = CGRect.init(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height)
    sceneView.delegate = self
    sceneView.showsStatistics = true
    self.view.addSubview(sceneView)

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

// 進入時開始執行
override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    let configuration = ARFaceTrackingConfiguration()
    sceneView.session.run(configuration)
}

// 退出時暫停
override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    sceneView.session.pause()
}

ARSCNViewDelegate

// 設置node
func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
        let faceMesh = ARSCNFaceGeometry(device: sceneView.device!)
        var node = SCNNode(geometry: faceMesh)
        return node
}
// 更新回傳
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {

}

BlendShapes 表情

表情偵測
以下小範例

// 張嘴巴
func expression(anchor: ARFaceAnchor) {
    let jawOpen = anchor.blendShapes[.jawOpen]
    if jawOpen?.decimalValue ?? 0.0 > 0.1 {
        DispatchQueue.main.async {
            self.view.showToast(text: "張嘴巴")
        }
    }
}

更多表情可參考以下更改
decimalValue決定表情大小

表情定位符描述
eyeBlinkLeft左眼眨眼
eyeLookDownLeft左眼目視下方
eyeLookInLeft左眼注視鼻尖
eyeLookOutLeft左眼向左看
eyeLookUpLeft左眼目視上方
eyeSquintLeft左眼眯眼
eyeWideLeft左眼睜大
eyeBlinkRight右眼眨眼
eyeLookDownRight右眼目視下方
eyeLookInRight右眼注視鼻尖
eyeLookOutRight右眼向左看
eyeLookUpRight右眼目視上方
eyeSquintRight右眼眯眼
eyeWideRight右眼睜大
jawForward努嘴時下巴向前
jawLeft撇嘴時下巴向左
jawRight撇嘴時下巴向右
jawOpen張嘴時下巴向下
mouthClose閉嘴
mouthFunnel稍張嘴並雙唇張開
mouthPucker抿嘴
mouthLeft向左撇嘴
mouthRight向右撇嘴
mouthSmileLeft左撇嘴笑
mouthSmileRight右撇嘴笑
mouthFrownLeft左嘴唇下壓
mouthFrownRight右嘴唇下壓
mouthDimpleLeft左嘴唇向後
mouthDimpleRight右嘴唇向後
mouthStretchLeft左嘴角向左
mouthStretchRight右嘴角向右
mouthRollLower下嘴唇卷向里
mouthRollUpper下嘴唇卷向上
mouthShrugLower下嘴唇向下
mouthShrugUpper上嘴唇向上
mouthPressLeft下嘴唇壓向左
mouthPressRight下嘴唇壓向右
mouthLowerDownLeft下嘴唇壓向左下
mouthLowerDownRight下嘴唇壓向右下
mouthUpperUpLeft上嘴唇壓向左上
mouthUpperUpRight上嘴唇壓向右上
browDownLeft左眉向外
browDownRight右眉向外
browInnerUp蹙眉
browOuterUpLeft左眉向左上
browOuterUpRight右眉向右上
cheekPuff臉頰向外
cheekSquintLeft左臉頰向上並回旋
cheekSquintRight右臉頰向上並回旋
noseSneerLeft左蹙鼻子
noseSneerRight右蹙鼻子
tongueOut吐舌頭

臉部矛點 臉部定位

func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
    let faceMesh = ARSCNFaceGeometry(device: sceneView.device!)
    let node = SCNNode(geometry: faceMesh)

    // 臉上矛點
    let clearMaterial = SCNMaterial(color: .link)
    node.geometry!.materials = [clearMaterial]
    node.geometry?.firstMaterial?.fillMode = .lines

    for x in 0..<1220 {
        let text = SCNText(string: "\(x)", extrusionDepth: 1)
        let txtnode = SCNNode(geometry: text)
        txtnode.scale = SCNVector3(x: 0.0002, y: 0.0002, z: 0.0002)
        txtnode.name = "\(x)"
        node.addChildNode(txtnode)
        txtnode.geometry?.firstMaterial?.fillMode = .fill
    }

    return node
}
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
    if let faceAnchor = anchor as? ARFaceAnchor, let faceGeometry = node.geometry as? ARSCNFaceGeometry {
        faceGeometry.update(from: faceAnchor.geometry)
        expression(anchor: faceAnchor)

        DispatchQueue.main.async {
            self.faceLabel.text = self.analysis
        }
    }

    guard let faceAnchor = anchor as? ARFaceAnchor,
          let faceGeometry = node.geometry as? ARSCNFaceGeometry
    else {
        return
    }

    // 臉上矛點
    for x in 0..<1220 {
        let child = node.childNode(withName: "\(x)", recursively: false)
        child?.position = SCNVector3(faceAnchor.geometry.vertices[x])
    }

    faceGeometry.update(from: faceAnchor.geometry)

}

DEMO

顯示每個臉部節點數字+網格

臉部特效

宣告圖片名稱+臉部座標

let noseOptions = ["nose_1", "nose_2", "nose_3", "nose_4"]
let eyeOptions = ["eye_1", "eye_2", "eye_3"]
let mouthOptions = ["mouth_1", "mouth_2", "mouth_3", "mouth_4"]
let hatOptions = ["hat_1", "hat_2", "hat_3", "hat_4"]
let features = ["nose", "leftEye", "rightEye", "mouth", "hat"]
let featureIndices = [[9], [1064], [42], [24, 25], [20]]
// 處理不同臉部表情位置
func updateFeatures(for node: SCNNode, using anchor: ARFaceAnchor) {
    for (feature, indices) in zip(features, featureIndices) {
        let child = node.childNode(withName: feature, recursively: false) as? ImageNode
        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.2, 0.8 + jawOpenValue, 1.2)
            case "hat":
                child?.scale = SCNVector3(1.0, 1.0, 1.0)

            default:
                break
        }
    }
}
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
    if let faceAnchor = anchor as? ARFaceAnchor, let faceGeometry = node.geometry as? ARSCNFaceGeometry {
        faceGeometry.update(from: faceAnchor.geometry)
        expression(anchor: faceAnchor)

        DispatchQueue.main.async {
            self.faceLabel.text = self.analysis
        }
    }

    guard let faceAnchor = anchor as? ARFaceAnchor else {
        return
    }

    updateFeatures(for: node, using: faceAnchor)
}
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 = ImageNode(with: noseOptions)
    noseNode.name = "nose"
    node.addChildNode(noseNode)

    let leftEyeNode = ImageNode(with: eyeOptions)
    leftEyeNode.name = "leftEye"
    leftEyeNode.rotation = SCNVector4(0, 1, 0, GLKMathDegreesToRadians(180.0))
    node.addChildNode(leftEyeNode)

    let rightEyeNode = ImageNode(with: eyeOptions)
    rightEyeNode.name = "rightEye"
    node.addChildNode(rightEyeNode)

    let mouthNode = ImageNode(with: mouthOptions)
    mouthNode.name = "mouth"
    node.addChildNode(mouthNode)

    let hatNode = ImageNode(with: hatOptions)
    hatNode.name = "hat"
    node.addChildNode(hatNode)

    updateFeatures(for: node, using: faceAnchor)

    return node
}

ImageNode

class ImageNode: 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 = UIImage.init(named: options.first ?? "")
        plane.firstMaterial?.isDoubleSided = true

        geometry = plane
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

// MARK: - Custom functions

extension ImageNode {
    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 = UIImage.init(named: options[index])
            plane.firstMaterial?.isDoubleSided = true
        }
    }
}

DEMO

搖頭/點頭偵測

var isLeft = false
var isRight = false
var isUp = false
var isDown = false
/// 檢查搖頭
func checkShakingHead() {
    if isLeft && isRight {
        DispatchQueue.main.async {
            self.view.showToast(text: "搖頭成立")
        }

        isLeft = false
        isRight = false
    }
}

/// 檢查點頭
func checkNod() {
    if isUp && isDown {
        DispatchQueue.main.async {
            self.view.showToast(text: "點頭成立")
        }

        isUp = false
        isDown = false
    }
}
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
    if let faceAnchor = anchor as? ARFaceAnchor, let faceGeometry = node.geometry as? ARSCNFaceGeometry {
        faceGeometry.update(from: faceAnchor.geometry)
        expression(anchor: faceAnchor)

        DispatchQueue.main.async {
            self.faceLabel.text = self.analysis
        }
    }

    // 搖頭偵測
    if node.orientation.y > 0.2 {
        isLeft = true
    } else if node.orientation.y < -0.2 {
        isRight = true
    }
    checkShakingHead()

    // 點頭偵測
    if node.orientation.x > 0.2 {
        isUp = true
    } else if node.orientation.x < -0.03 {
        isDown = true
    }
    checkNod()
}

DEMO

專案下載:
Github

參考文件:
AR Face Tracking Tutorial for iOS: Getting Started
https://www.raywenderlich.com/5491-ar-face-tracking-tutorial-for-ios-getting-started

ARFoundation之路-人脸检测增强之四
https://blog.csdn.net/yolon3000/article/details/101388015

Swift更多文章

Swift 彈出視窗 AlertController 的使用方法 💥

Swift 判斷螢幕方向 📱

Swift Core Data 實現 💾🔥

Swift UISegmentedControl 💻分段控制器!

Swift 實現抽屜效果 🧹

Categorized in: