最近用过了支付宝的人脸识别登录,作为一个技术人员应该保持对技术探索、追求的学习心态。这两天就搜集各方资料学习了一下,但好像走了弯路/(ㄒoㄒ)/~~!!!这篇笔记记录一下自己的学习。
1、体验分析
首先思考的应该是人脸识别登录的流程。本人第一次体验到的人脸识别技术的应用当然是支付宝。
支付宝开启人脸登录流程:首先是账号密码登录 ----> 然后在app内根据提示完成信息采集 ----> 信息采集完成后下次登录时就可以使用该功能了!
然后又找到另一款带有人脸识别登录功能app应用,其开启流程:账号密码登录后 ----> 在相关功能模块调用相机,拍摄较清晰的人脸照片进行上传 ----> 之后就可以使用了!!!
分析其登录流程:第一步都是要密码验证登录,第二步需要采集当前账号的所属人面部信息,然后才可使用该功能。(应该算是一种辅助登录手段,毕竟没有使用人脸注册账号。但是真的很好用,省去了输入密码或忘记密码的麻烦)
在移动端开发此功能,(密码登录直接略过)我们应该首先考虑的是信息采集时的面部信息。那么第一个要解决的问题就是识别出人脸,其次将信息上传至后台与该账号关联。下次登录时,将扫描到的人脸图像上传至后台与存储的数据对比,返回登录结果。(因为未找到具体的相关资料,此分析目前仅是一个移动开发者的分析,如有漏洞下次更新)
2、移动端的人脸识别
根据之前的分析,我们需要在本地分析采集到的图像是否有人脸信息。根据查找的资料,有如下实现方法:1.苹果原生CoreImage.framework框架。2.OpenVC框架,该框架是一个跨平台框架,专注于做图像技术的处理。(更多介绍百度即可)3.AVFoundation框架,这是在使用CoreImage走了弯路时发现的。
(1)CoreImage.framework
我们先用该框架实现人脸识别。首先创建一个工程,导入 CoreImage.framework 库:
创建一个UIImageView,给其一张图片,我们直接用storyboard拖拽界面、设置坐标,并关联属性到控制器(步骤简单略过):
此时要注意,ImageView的图片适应模式设置为scaleAspectFit,如果要将图片拉伸展示到ImageView的话,面部位置并不在计算到的坐标框内。
插入识别代码:
func detectFace(withImage image: UIImage) {// 将图像转为CIImage,使用Core Image需要使用CIImageguard let personCIImg = CIImage(image: image) else {return}// 设置识别精度let opts: [String: Any] = [CIDetectorAccuracy: CIDetectorAccuracyHigh]// 初始化识别器let detector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: opts)let result: [CIFaceFeature] = (detector?.features(in: personCIImg, options: opts) as? [CIFaceFeature])!if result.count > 0 {for face in result {let faceBox = UIView(frame: face.bounds)// 画一个红框画出面部位置faceBox.layer.borderWidth = 3faceBox.layer.borderColor = UIColor.red.cgColorfaceBox.backgroundColor = UIColor.clear// 添加红框到图片上imgView.addSubview(faceBox)print("面部坐标------> %d ", faceBox.frame)}}}
好了,代码看似很简单的:
-
#3行:从imgView变量中取出image并转为CIImage,因为使用Core Image时需要用CIImage。
-
#9行:初始化一个识别器,并将识别精度以字典参数给它。
-
#11行:调用识别器的识别方法,并保存一个 [CIFaceFeature] 类型的结果。
-
#13——#25行:从返回的结果中取出数据。点开 CIFaceFeature 类我们可以看到的它的属性值,bounds就是我们要的识别到的面部区域。
-
#17——#22行:将识别到的区域用红框显示出来。
接下来调用测试一下代码结果:
imgView.contentMode = .scaleAspectFitself.detectFace(withImage: imgView.image!)
运行结果看到红框出界了,并没有出现在预期的范围内,这是因为UIKit框架中使用的坐标与Core Image 的坐标不同造成,为此我们还需要将坐标转换。
将转换代码加入会,代码如下:
func detectFace(withImage image: UIImage) {// 将图像转为CIImage,使用Core Image需要使用CIImageguard let personCIImg = CIImage(image: image) else {return}// 设置识别精度let opts: [String: Any] = [CIDetectorAccuracy: CIDetectorAccuracyHigh]// 初始化识别器let detector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: opts)let result: [CIFaceFeature] = (detector?.features(in: personCIImg, options: opts) as? [CIFaceFeature])!let CIImgSize = personCIImg.extent.sizevar transform = CGAffineTransform(scaleX: 1, y: -1)transform = transform.translatedBy(x: 0, y: -CIImgSize.height)if result.count > 0 {for face in result {// Apply the transform to convert the coordinatesvar faceViewBounds = face.bounds.applying(transform)// Calculate the actual position and size of the rectangle in the image viewlet viewSize = imgView.bounds.sizelet scale = min(viewSize.width / CIImgSize.width,viewSize.height / CIImgSize.height)let offsetX = (viewSize.width - CIImgSize.width * scale) / 2let offsetY = (viewSize.height - CIImgSize.height * scale) / 2faceViewBounds = faceViewBounds.applying(CGAffineTransform(scaleX: scale, y: scale))faceViewBounds.origin.x += offsetXfaceViewBounds.origin.y += offsetYlet faceBox = UIView(frame: faceViewBounds)// 画一个红框画出面部位置faceBox.layer.borderWidth = 3faceBox.layer.borderColor = UIColor.red.cgColorfaceBox.backgroundColor = UIColor.clear// 添加红框到图片上imgView.addSubview(faceBox)print("面部坐标------> %d ", faceBox.frame)if face.rightEyeClosed {print("右眼闭着")}if face.leftEyeClosed {print("左眼闭着")}if face.hasSmile {print("在笑")}}}}
-
#45行——#53行还可以识别图像的动作,具体的可以到类中查看。
运行效果:
到这里,我们已经可以识别出人脸信息,但是在使用时我们不能拿着“这么大”的照片去用,裁剪后使用效率应该会更高。剔除人脸外无用的信息。但是进行的并不是很顺利,因为裁剪区域的坐标和界面显示的红框坐标并不相同,无奈被卡到现在还并未解决。
此处被卡时间太长,暂且搁置,后续研究
到这我们仅仅完成了静态人物的面部识别,而调用摄像头后动态面部信息的捕捉也是我们要使用的技术问题。前面所说的走弯路就在这了,看到了CoreImage.framework 可以识别图像,又可以自定义拍照界面,所以沿着这条路走了下去,结果并没有走通。首先说自定义界面只是简单的自定义,并不能按照自己的UI设计搭建拍照界面。其二,动态识别数据流信息(抓取人像信息,而不是拍摄后识别)貌似也不可以!!!
还好,在解这些问题的时候发现AVFoundation框架使我们需要的。
(2)AVFoundation.framework
正在研究中。。。
注:记得在plist文件中添加权限声明(调用相机,麦克风,媒体库等都要添加对应的权限),否则会崩的
直接上代码再注释吧,网上系统的资料比较少,基本都是一些简单的代码,也不能直接拿过来用!
var session: AVCaptureSession?var device: AVCaptureDevice?var input: AVCaptureDeviceInput?var output: AVCaptureMetadataOutput?var preview: AVCaptureVideoPreviewLayer?// 初始化相机func setupCamera() {//sessionsession = AVCaptureSession()session?.sessionPreset = AVCaptureSessionPresetPhotosession?.sessionPreset = AVCaptureSessionPreset1280x720// devicedevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)// 设置为前置摄像头let devices = AVCaptureDeviceDiscoverySession(deviceTypes: [AVCaptureDeviceType.builtInWideAngleCamera], mediaType: AVMediaTypeVideo, position: AVCaptureDevicePosition.front)for device in (devices?.devices)! {self.device = device}// inputdo {try input = AVCaptureDeviceInput(device: device)} catch let error as NSError {print("error: \(error.localizedDescription)")}if (session?.canAddInput(input))! {session?.addInput(input)}// output // 人脸识别output = AVCaptureMetadataOutput()output?.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)if (session?.canAddOutput(output))! {session?.addOutput(output)}output?.metadataObjectTypes = [AVMetadataObjectTypeFace]// previewpreview = AVCaptureVideoPreviewLayer(session: session)preview?.videoGravity = AVLayerVideoGravityResizeAspectFillpreview?.frame = CGRect(x: (self.view.frame.width-200)/2.0, y: 150, width: 200, height: 200)preview?.cornerRadius = 100preview?.borderColor = UIColor.gray.cgColorpreview?.borderWidth = 3// preview?.frame = self.view.boundsself.view.layer.insertSublayer(preview!, at: 0)session?.startRunning()}
-
#3行,#17——#24行:硬件设备声明和初始化,调用前置摄像头
-
#5行:input,输入流
-
#7行:output,输出流
-
#9行:preview,预览界面,可以设置为想要的扫描效果界面。
-
#54行:初始化完成后一定要启动session
注:此处有一个巨大的坑,在设置output输出流,添加扫描识别类型时,一定要先添加input输入流,否则崩掉,然后网上也找不到相关资料,这是自己一步步试出来的!!!
上边的初始化完成后,需要实现一个协议方法,该方法在扫描到有人脸时会持续返回数据:
// MARK: - AVCaptureMetadataOutputObjectsDelegatefunc captureOutput(_ captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from connection: AVCaptureConnection!) {for item in metadataObjects {if (item as! AVMetadataObject).type == AVMetadataObjectTypeFace {let transform: AVMetadataObject = (preview?.transformedMetadataObject(for: item as? AVMetadataObject))!DispatchQueue.global().async {DispatchQueue.main.async {self.showFaceImage(withFrame: transform.bounds)}}}}}/// 显示人脸位置视图func showFaceImage(withFrame rect: CGRect) {if isStartFaceRecognition {isStartFaceRecognition = falsefaceBoxView.frame = rectself.faceBoxView.transform = CGAffineTransform(scaleX: 1.5, y: 1.5);UIView.animate(withDuration: 0.3, animations: {[weak self] inself?.faceBoxView.alpha = 1.0self?.faceBoxView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0);}) { (finished: Bool) inUIView.animate(withDuration: 0.2, animations: {[weak self] inself?.faceBoxView.alpha = 0.0}, completion: { (finished: Bool) inself.isStartFaceRecognition = true})}}}