Vision 初探 — APP 頭像上傳 自動識別人臉裁圖 (Swift)
Vision 實戰應用
[2024/08/13 Update]
- 請參考新文章、新的 API:「 iOS Vision framework x WWDC 24 Discover Swift enhancements in the Vision framework Session 」
一樣不多說,先上一張成品圖:
優化前 V.S 優化後 — 結婚吧APP
前陣子iOS 12發佈更新,注意到新開放的CoreML 機器學習框架;覺得挺有趣的,就開始構想如果想用在當前的產品上能放在哪裡?
CoreML嚐鮮文章現已發佈: 使用機器學習自動預測文章分類,連模型也自己訓練
CoreML提供文字、圖像的機器學習模型訓練及引用到APP裡的接口,我原先的想法是,使用CoreML來做到人臉識別,解決APP中有裁圖的項目頭或臉被卡掉的問題,如上圖左所示,若人臉出現在周圍則很容易因為縮放+裁圖造成臉不完整.
經過網路搜尋一番後才發現我學識短淺,這個功能在iOS 11就已發佈:「Vision」框架,支援文字偵測、人臉偵測、圖像比對、QRCODE偵測、物件追蹤…功能
這邊使用的就是其中的人臉偵測項目,經優化後如右圖所示;找到人臉並以此為中心裁圖.
實戰開始:
首先我們先做能標記人臉位置的功能,初步認識一下Vision怎麼用
Demo APP
完成圖如上所示,能標記出照片中人臉的位置
p.s 僅能標記「人臉」,整個頭包含頭髮並不行😅
這塊程式主要分為兩部分,第一部分要解決 圖片原尺寸縮放放入 ImageView時會留白的狀況;簡單來說我們要的是Image的Size多大,ImageView的Size就有多大,若直接放入圖片會造成如下走位情形
你可能會想說直接改ContentMode變成fill、fit、redraw,但就會變形或圖片被卡掉
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let ratio = UIScreen.main.bounds.size.width
//這邊是因為我UIIMAGEVIEW 那邊設定左右對齊0,寬高比1:1
let sourceImage = UIImage(named: "Demo2")?.kf.resize(to: CGSize(width: ratio, height: CGFloat.leastNonzeroMagnitude), for: .aspectFill)
//使用KingFisher的圖片變形功能,已寬為基準,高度自由
imageView.contentMode = .redraw
//contentMode使用redraw填滿
imageView.image = sourceImage
//賦予圖片
imageViewConstraints.constant = (ratio - (sourceImage?.size.height ?? 0))
imageView.layoutIfNeeded()
imageView.sizeToFit()
//這一塊是我去改變 imageView的Constraints,詳情可看文末完整範例
以上就是針對圖片做的處理
裁圖部分使用Kingfisher幫助我們,也可替換成其他套件或自刻方法
第二部分,進入重點直接看Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
if #available(iOS 11.0, *) {
//iOS 11之後才支援
let completionHandle: VNRequestCompletionHandler = { request, error in
if let faceObservations = request.results as? [VNFaceObservation] {
//辨識到的臉臉們
DispatchQueue.main.async {
//操作UIVIEW,切回主執行緒
let size = self.imageView.frame.size
faceObservations.forEach({ (faceObservation) in
//坐標系轉換
let translate = CGAffineTransform.identity.scaledBy(x: size.width, y: size.height)
let transform = CGAffineTransform(scaleX: 1, y: -1).translatedBy(x: 0, y: -size.height)
let transRect = faceObservation.boundingBox.applying(translate).applying(transform)
let markerView = UIView(frame: transRect)
markerView.backgroundColor = UIColor.init(red: 0/255, green: 255/255, blue: 0/255, alpha: 0.3)
self.imageView.addSubview(markerView)
})
}
} else {
print("未偵測到任何臉")
}
}
//辨識請求
let baseRequest = VNDetectFaceRectanglesRequest(completionHandler: completionHandle)
let faceHandle = VNImageRequestHandler(ciImage: ciImage, options: [:])
DispatchQueue.global().async {
//辨識需要時間,所以放入背景子執行緒執行,避免當前畫面卡住
do{
try faceHandle.perform([baseRequest])
}catch{
print("Throws:\(error)")
}
}
} else {
//
print("不支援")
}
主要要注意的是,坐標系轉換部分;辨識出來的結果是Image的原始座標;我們須將它轉換成包在外面的ImageView的實際座標才能正確地使用它.
再來我們來做今天的重頭戲 — 依照人臉的位置裁切出大頭貼的正確位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
let ratio = UIScreen.main.bounds.size.width
//這邊是因為我UIIMAGEVIEW 那邊設定左右對齊0,寬高比1:1,詳情可看文末完整範例
let sourceImage = UIImage(named: "Demo")
imageView.contentMode = .scaleAspectFill
//使用scaleAspectFill模式填滿
imageView.image = sourceImage
//直接賦予原圖片,我們之後再操作
if let image = sourceImage,#available(iOS 11.0, *),let ciImage = CIImage(image: image) {
let completionHandle: VNRequestCompletionHandler = { request, error in
if request.results?.count == 1,let faceObservation = request.results?.first as? VNFaceObservation {
//ㄧ張臉
let size = CGSize(width: ratio, height: ratio)
let translate = CGAffineTransform.identity.scaledBy(x: size.width, y: size.height)
let transform = CGAffineTransform(scaleX: 1, y: -1).translatedBy(x: 0, y: -size.height)
let finalRect = faceObservation.boundingBox.applying(translate).applying(transform)
let center = CGPoint(x: (finalRect.origin.x + finalRect.width/2 - size.width/2), y: (finalRect.origin.y + finalRect.height/2 - size.height/2))
//這裡是計算臉的範圍中間點位置
let newImage = image.kf.resize(to: size, for: .aspectFill).kf.crop(to: size, anchorOn: center)
//將圖片依照中間點裁切
DispatchQueue.main.async {
//操作UIVIEW,切回主執行緒
self.imageView.image = newImage
}
} else {
print("偵測到多張臉或沒有偵測到臉")
}
}
let baseRequest = VNDetectFaceRectanglesRequest(completionHandler: completionHandle)
let faceHandle = VNImageRequestHandler(ciImage: ciImage, options: [:])
DispatchQueue.global().async {
do{
try faceHandle.perform([baseRequest])
}catch{
print("Throws:\(error)")
}
}
} else {
print("不支援")
}
道理跟標記人臉位置差不多,差別在大頭貼的部分是固定尺寸(如:300x300),所以我們略過前面需要讓Image適應ImageView的第一部分
另一個差別是我們要多計算人臉範圍的中心點,並以這個中心點為準做裁切圖片
紅點為臉的範圍中心點
完成效果圖:
頓丹前的那一秒是原始圖位置
完整APP範例:
程式碼已上傳至Github: 請點此
有任何問題及指教歡迎 與我聯絡 。
===
View the English version of this article here.
本文首次發表於 Medium ➡️ 前往查看