Vision框架Swift教学|APP自动人脸识别裁图实作指南
针对iOS APP开发者,解决头像裁图时人脸被截断问题,透过Vision框架自动定位人脸并精准裁切,提升用户头像呈现品质,实现人脸中心点裁图,优化APP使用体验。
Click here to view the English version of this article.
點擊這裡查看本文章正體中文版本。
基于 SEO 考量,本文标题与描述经 AI 调整,原始版本请参考内文。
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: 请点此
有任何问题及指教欢迎 与我联络 。
本文首次发表于 Medium (点击查看原始版本),由 ZMediumToMarkdown 提供自动转换与同步技术。