ZhgChg.Li

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

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

Vision框架Swift教学|APP自动人脸识别裁图实作指南
本文使用 AI 翻译,如有不妥敬请告知。"

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

Vision 实战应用

[2024/08/13 Update]

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

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

优化前 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,但就会变形或图片被卡掉

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

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的实际座标才能正确地使用它.

再来我们来做今天的重头戏 — 依照人脸的位置裁切出大头贴的正确位置

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

在 GitHub 上补充修正
编辑这篇文章
本文首次发表于 Medium
点此查看原文
分享这篇文章
复制链接 · 分享到社群
ZhgChgLi
作者

ZhgChgLi

An iOS, web, and automation developer from Taiwan 🇹🇼 who also loves sharing, traveling, and writing.

留言 · Comments