最近有看到一些拍照應用提供人臉變老預測的功能,體驗下來趣味性不錯,決定本身嘗試實現一下。通過網上一番搜索,沒看到有完整方案開源實現可供參考,一些相關的博客大都是簡單的說起一些思路和給出一些效果圖,因而在借鑑了一些前人的思路以後,本身實現了一我的臉變老的方案,項目代碼和算法相關均由 Swift 實現, 現將具體的實現步驟和核心的代碼分享一下。完整的 Demo 代碼會附在本文末尾,最終的效果圖以下: php
該方案實現的原理是將一張預製做好的皺紋紋理「貼在」原圖的人臉區域上,聽起來很簡單,不過在具體實現上則須要考慮很多問題,讓咱們從後往前去推導哪些要須要解決的問題:首先,預製做好的皺紋紋理如何和原圖中的人臉天然的貼合?考慮到不一樣原圖中的人臉膚色和亮度會有很大的差別,若是針對不一樣的膚色來提供不一樣的皺紋紋理顯然是不可行的。其次,預製好的皺紋紋理的五官區域明顯是和原圖中的人臉不符合,那麼就須要針對不一樣的人臉特徵點來對皺紋紋理進行複雜變形。考慮到以上種種,本方案的實現步驟分爲如下三步:git
讓咱們一步步來實現:github
這一步的實現方案比較簡單,藉助的是 Face++ 平臺的技術實現,只須要簡單的申請註冊就能夠無償使用人臉識別功能,客戶端只須要上傳圖片調用相關的Api便可,返回的人臉識別特徵點信息大體以下圖所示(圖片源自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 變形算法還可以用來實現更多的功能,譬如美顏瘦臉、大眼、換裝等,拓展性高。
本次的分享就到這啦,喜歡的話能夠點個贊👍或關注。若有錯誤的地方歡迎你們在評論裏指出。
本文爲我的原創,轉載請註明出處。