【乾貨】開源一我的臉變老方案實現(Swift)

最近有看到一些拍照應用提供人臉變老預測的功能,體驗下來趣味性不錯,決定本身嘗試實現一下。通過網上一番搜索,沒看到有完整方案開源實現可供參考,一些相關的博客大都是簡單的說起一些思路和給出一些效果圖,因而在借鑑了一些前人的思路以後,本身實現了一我的臉變老的方案,項目代碼和算法相關均由 Swift 實現, 現將具體的實現步驟和核心的代碼分享一下。完整的 Demo 代碼會附在本文末尾,最終的效果圖以下: php

人臉變老實現

該方案實現的原理是將一張預製做好的皺紋紋理「貼在」原圖的人臉區域上,聽起來很簡單,不過在具體實現上則須要考慮很多問題,讓咱們從後往前去推導哪些要須要解決的問題:首先,預製做好的皺紋紋理如何和原圖中的人臉天然的貼合?考慮到不一樣原圖中的人臉膚色和亮度會有很大的差別,若是針對不一樣的膚色來提供不一樣的皺紋紋理顯然是不可行的。其次,預製好的皺紋紋理的五官區域明顯是和原圖中的人臉不符合,那麼就須要針對不一樣的人臉特徵點來對皺紋紋理進行複雜變形。考慮到以上種種,本方案的實現步驟分爲如下三步:git

  • 一、識別圖片中的人臉區域並提取人臉特徵點
  • 二、根據人臉特徵點來對皺紋紋理的各區域進行復雜變形
  • 三、將變形後的皺紋紋理天然的貼合在原圖識別出的人臉區域上

讓咱們一步步來實現:github

識別人臉關鍵點

這一步的實現方案比較簡單,藉助的是 Face++ 平臺的技術實現,只須要簡單的申請註冊就能夠無償使用人臉識別功能,客戶端只須要上傳圖片調用相關的Api便可,返回的人臉識別特徵點信息大體以下圖所示(圖片源自Face++): 算法

Face++

對皺紋紋理進行變形處理

提取皺紋紋理特徵點座標

變形前須要先獲取皺紋紋理上對應的人臉特徵點座標,因爲皺紋紋理是提早準備的,因此能夠直接經過獲取圖片點座標工具來提取特性點座標數據: swift

變形算法實現

考慮到這是基於特徵點的複雜變形,因此皺紋紋理圖片的渲染選擇了用 OpenGL,iOS SDK 提供了封裝好的 GLKit 來方便使用 OpenGL ,只須要建立一個 GLKViewController: 數組

而後重寫 glkView 方法:

import UIKit
import GLKit
class FaceGLKViewController: GLKViewController {
    ···
    override func glkView(_ view: GLKView, drawIn rect: CGRect) {
        ···
    }
    ···
}
複製代碼

  新建一個 ImageMesh 類,用來記錄皺紋紋理內座標網格點信息:markdown

class ImageMesh: NSObject {
    var verticalDivisions = 0
    var horizontalDivisions = 0
    var indexArrSize = 0
    var vertexIndices: [Int]? = nil
    // Opengl座標點數組
    var verticesArr: [Float]? = nil
    var textureCoordsArr: [Float]? = nil
    var texture: GLKTextureInfo? = nil
    var image_width: Float = 0.0
    var image_height: Float = 0.0
    var numVertices: Int = 0
    var xy: [vector_float2]? = nil
    var ixy: [vector_float2]? = nil
    
    convenience init(vd: Int, hd: Int) {
        self.init()
        verticalDivisions = vd
        horizontalDivisions = hd
        numVertices = (verticalDivisions + 1) * (horizontalDivisions + 1)
        indexArrSize = 2 * verticalDivisions * (horizontalDivisions + 1)
        verticesArr = [Float](repeating: 0.0, count: 2 * indexArrSize)
        textureCoordsArr = [Float](repeating: 0.0, count: 2 * indexArrSize)
        vertexIndices = [Int](repeating: 0, count: indexArrSize)
        xy = [vector_float2](repeating: [0.0, 0.0], count: numVertices)
        ixy = [vector_float2](repeating: [0.0, 0.0], count: numVertices)
        var count = 0
        for i in 0..<verticalDivisions {
            for j in 0...horizontalDivisions {
                vertexIndices![count] = (i + 1) * (horizontalDivisions + 1) + j; count += 1
                vertexIndices![count] = i * (horizontalDivisions + 1) + j; count += 1
            }
        }
        let xIncrease = 1.0 / Float(horizontalDivisions)
        let yIncrease = 1.0 / Float(verticalDivisions)
        count = 0
        for i in 0..<verticalDivisions {
            for j in 0...horizontalDivisions {
                let currX = Float(j) * xIncrease;
                let currY = 1 - Float(i) * yIncrease;
                textureCoordsArr![count] = currX; count += 1
                textureCoordsArr![count] = currY - yIncrease; count += 1
                textureCoordsArr![count] = currX; count += 1
                textureCoordsArr![count] = currY; count += 1
            }
        }
    }
    ···
}
複製代碼

而後調用 Opengl Api 完成渲染工做:app

override func glkView(_ view: GLKView, drawIn rect: CGRect) {
    // 透明背景
    glClearColor(0.0, 0.0, 0.0, 0.0)
    glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
    glBlendFunc(GLenum(GL_SRC_ALPHA), GLenum(GL_ONE_MINUS_SRC_ALPHA));
    glEnable(GLenum(GL_BLEND));
    if (isSetup) {
        renderImage()
    }
}

func renderImage() {
    self.effect?.texture2d0.name = (mainImage?.texture?.name)!
    self.effect?.texture2d0.enabled = GLboolean(truncating: true)
    self.effect?.prepareToDraw()

    glEnableVertexAttribArray(GLuint(GLKVertexAttrib.position.rawValue))
    glEnableVertexAttribArray(GLuint(GLKVertexAttrib.texCoord0.rawValue))
    glVertexAttribPointer(GLuint(GLKVertexAttrib.position.rawValue), 2, GLenum(GL_FLOAT), GLboolean(GL_FALSE), 8, mainImage?.verticesArr)
    glVertexAttribPointer(GLuint(GLKVertexAttrib.texCoord0.rawValue), 2, GLenum(GL_FLOAT), GLboolean(GL_FALSE), 8, mainImage?.textureCoordsArr)

    for i in 0..<(mainImage?.verticalDivisions)! {
        glDrawArrays(GLenum(GL_TRIANGLE_STRIP), GLint(i * (self.mainImage!.horizontalDivisions * 2 + 2)), GLsizei(self.mainImage!.horizontalDivisions * 2 + 2))
    }
}
複製代碼

接下來是實現基於關鍵點的變形,變形的算法實現是根據 Image Deformation Using Moving Least Squares 論文來編寫的,論文的內容和推導過程比較簡潔,側重於給出最終的數學公式,有興趣的能夠去詳讀。爲了方便,本方案用 Swift 來實現該算法。以皺紋紋理上的特徵點做爲變形原點, Face++ 返回的人臉特徵點做爲變形目標點,對皺紋紋理進行變形:ide

func setupImage(image: UIImage, width: CGFloat, height: CGFloat, original_vertices: [float2], target_vertices: [float2]) {
    let _ = mainImage?.loadImage(image: image, width: width, height: height)
    setupViewSize()
    let count = target_vertices.count
    var p = original_vertices
    // 轉換座標系
    for i in 0..<count {
        p[i] = [p[i].x - Float(image.size.width / 2), Float(image.size.height / 2) - p[i].y]
        p[i] = [p[i].x * Float(width) / Float(image.size.width), p[i].y * Float(height) / Float(image.size.height)]
    }

    let q = target_vertices
    var w = [Float](repeating: 0.0, count: count)
    
    // 計算變形權重
    for i in 0..<(self.mainImage?.numVertices)! {
        var ignore = false
        for j in 0..<count {
            let distanceSquare = ((self.mainImage?.ixy![i])! - p[j]).squaredNorm()
            if distanceSquare < 10e-6 {
                self.mainImage?.xy![i] = p[j]
                ignore = true
            }

            w[j] = 1 / distanceSquare
        }

        if ignore {
            continue
           }

        var pcenter = vector_float2()
        var qcenter = vector_float2()
        var wsum: Float = 0.0
        for j in 0..<count {
            wsum += w[j]
            pcenter += w[j] * p[j]
            qcenter += w[j] * q[j]
        }

        pcenter /= wsum
        qcenter /= wsum

        var ph = [vector_float2](repeating: [0.0, 0.0], count: count)
        var qh = [vector_float2](repeating: [0.0, 0.0], count: count)
        for j in 0..<count {
            ph[j] = p[j] - pcenter
            qh[j] = q[j] - qcenter
        }
            
        // 開始矩陣變換
        var M = matrix_float2x2()
        var P: matrix_float2x2? = nil
        var Q: matrix_float2x2? = nil
        var mu: Float = 0.0
        for j in 0..<count {
            P = matrix_float2x2([ph[j][0], ph[j][1]], [ph[j][1], -ph[j][0]])
            Q = matrix_float2x2([qh[j][0], qh[j][1]], [qh[j][1], -qh[j][0]])
            M += w[j] * Q! * P!
            mu += w[j] * ph[j].squaredNorm()
        }

        self.mainImage?.xy![i] = M * ((self.mainImage?.ixy![i])! - pcenter) / mu;
        self.mainImage?.xy![i] = ((self.mainImage?.ixy![i])! - pcenter).norm() * ((self.mainImage?.xy![i])!).normalized() + qcenter;
    }

    self.mainImage?.deform()

    isSetup = true
}
複製代碼

最終獲得變形後的皺紋紋理以下: 工具

皺紋紋理與人臉「貼合」

直接將皺紋紋理覆蓋在人臉上顯然是不可取的,咱們要作的是將人臉原圖和皺紋紋理進行適當的圖片混合

圖片混合經常使用的模式有不少種,如疊加、柔光、強光等,各混合模式的算法實現起來也都比較簡單,具體的算法公式能夠看這篇知乎總結:Photoshop圖層混合模式計算公式大全。更方便的是 CGContext 內置了這些經常使用的混合模式的實現,能夠直接經過 UIImage#draw 方法 調用,本人測試下來,柔光混合(soft light blend mode)的效果是最爲理想:

/// 人臉變老
///
/// - Parameters:
/// - face: 人臉圖片
/// - wrinkle: 皺紋紋理圖片
/// - faceRect: 人臉區域
/// - Returns: 合成結果
func softlightMerge(face: UIImage, wrinkle: UIImage, faceRect: CGRect) -> UIImage? {
    let rendererRect = CGRect(x: 0, y: 0, width: face.size.width, height: face.size.height)
    let renderer = UIGraphicsImageRenderer(bounds: rendererRect)
    let outputImage = renderer.image { ctx in
        UIColor.white.set()
        ctx.fill(rendererRect)
        face.draw(in: rendererRect, blendMode: .normal, alpha: 1)
        // 柔光混合
        wrinkle.draw(in: faceRect, blendMode: .softLight, alpha: 1)
    }
    return outputImage
}
複製代碼

通過柔光混合,無需考慮原圖人臉的膚色如何,混合後的人臉會保持原膚色,最後的效果以下:

總結

實現人臉變老的方案有不少,本人提出的方案,優勢在於不用考慮原圖人臉的膚色、亮度等因素,一張預製的皺紋臉皮便可適用於大多數的人的圖片,缺點則在於變老的效果僅體如今於有更多的「皺紋」,總體效果離真實變老有很多的差距。

在方案的實現上,使用了 Swfit 語言在 iOS 端實現,不過其中涉及的 Opengl 以及相關算法都可以輕鬆的在 Android 等其餘平臺復現,基於人臉特徵點的 mls 變形算法還可以用來實現更多的功能,譬如美顏瘦臉、大眼、換裝等,拓展性高。

項目源碼

本次的分享就到這啦,喜歡的話能夠點個贊👍或關注。若有錯誤的地方歡迎你們在評論裏指出。

本文爲我的原創,轉載請註明出處。

相關文章
相關標籤/搜索