Post

Vision框架Swift教学|APP自动人脸识别裁图实作指南

针对iOS APP开发者,解决头像裁图时人脸被截断问题,透过Vision框架自动定位人脸并精准裁切,提升用户头像呈现品质,实现人脸中心点裁图,优化APP使用体验。

Vision框架Swift教学|APP自动人脸识别裁图实作指南

Click here to view the English version of this article.

點擊這裡查看本文章正體中文版本。

基于 SEO 考量,本文标题与描述经 AI 调整,原始版本请参考内文。


Vision 初探 — APP 头像上传 自动识别人脸裁图 (Swift)

Vision 实战应用

[2024/08/13 Update]

一样不多说,先上一张成品图:

优化前 V.S 优化后 — [结婚吧APP](https://itunes.apple.com/tw/app/%E7%B5%90%E5%A9%9A%E5%90%A7-%E4%B8%8D%E6%89%BE%E6%9C%80%E8%B2%B4-%E5%8F%AA%E6%89%BE%E6%9C%80%E5%B0%8D/id1356057329?ls=1&mt=8){:target="_blank"}

优化前 V.S 优化后 — 结婚吧APP

前阵子iOS 12发布更新,注意到新开放的CoreML 机器学习框架;觉得挺有趣的,就开始构想如果想用在当前的产品上能放在哪里?

CoreML尝鲜文章现已发布: 使用机器学习自动预测文章分类,连模型也自己训练

CoreML提供文字、图像的机器学习模型训练及引用到APP里的接口,我原先的想法是,使用CoreML来做到人脸识别,解决APP中有裁图的项目头或脸被卡掉的问题,如上图左所示,若人脸出现在周围则很容易因为缩放+裁图造成脸不完整.

经过网路搜寻一番后才发现我学识短浅,这个功能在iOS 11就已发布:「Vision」框架,支援文字侦测、人脸侦测、图像比对、QRCODE侦测、物件追踪…功能

这边使用的就是其中的人脸侦测项目,经优化后如右图所示;找到人脸并以此为中心裁图.

实战开始:

首先我们先做能标记人脸位置的功能,初步认识一下Vision怎么用

Demo APP

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: 请点此

有任何问题及指教欢迎 与我联络


Buy me a beer

本文首次发表于 Medium (点击查看原始版本),由 ZMediumToMarkdown 提供自动转换与同步技术。

Improve this page on Github.

This post is licensed under CC BY 4.0 by the author.