2018-09-04 更新: 很久没有更新文章了,工作之余花时间看了之前写的这篇文章并运行了之前写的配套 Demo,通过打印人脸特征 CIFaceFeature 的属性,发现识别的效果并不是很好,具体说明见文章最底部的更新标题,后续我将分别用 OpenCV(跨平台计算机视觉库)和 Vision(iOS 11 新 API)两种库实现人脸面部识别,敬请期待~~
OC 版下载地址,swift 版下载地址
CoreImage 是 Cocoa Touch 中一个强大的 API,也是 iOS SDK 中的关键部分,不过它经常被忽视。在本篇教程中,我会带大家一起验证 CoreImage 的人脸识别特性。在开始之前,我们先要简单了解下 CoreImage framework 组成 CoreImage framework 组成 Apple 已经帮我们把 image 的处理分类好,来看看它的结构:主要分为三个部分:
1. 定义部分:CoreImage 和 CoreImageDefines。见名思义,代表了 CoreImage 这个框架和它的定义。2. 操作部分:
滤镜(CIFliter):CIFilter 产生一个 CIImage。典型的,接受一到多的图片作为输入,经过一些过滤操作,产生指定输出的图片。检测(CIDetector):CIDetector 检测处理图片的特性,如使用来检测图片中人脸的眼睛、嘴巴、等等。特征(CIFeature):CIFeature 代表由 detector 处理后产生的特征。
3. 图像部分:
画布(CIContext):画布类可被用与处理 Quartz 2D 或者 OpenGL。可以用它来关联 CoreImage 类。如滤镜、颜色等渲染处理。颜色(CIColor):图片的关联与画布、图片像素颜色的处理。向量(CIVector):图片的坐标向量等几何方法处理。图片(CIImage):代表一个图像,可代表关联后输出的图像。
在了解上述基本知识后,我们开始通过创建一个工程来带大家一步步验证 Core Image 的人脸识别特性。将要构建的应用
iOS 的人脸识别从 iOS 5(2011)就有了,不过一直没怎么被关注过。人脸识别 API 允许开发者不仅可以检测人脸,也可以检测到面部的一些特殊属性,比如说微笑或眨眼。首先,为了了解 Core Image 的人脸识别技术我们会创建一个 app 来识别照片中的人脸并用一个方框来标记它。在第二个 demo 中,让用户拍摄一张照片,检测其中的人脸并检索人脸位置。这样一来,就充分掌握了 iOS 中的人脸识别,并且学会如何利用这个强大却总被忽略的 API。话不多说,开搞! 建立工程(我用的是 Xcode8.0)
这里提供了初始工程,当然你也可以自己创建(主要是为了方便大家)点我下载 用 Xcode 打开下载后的工程,可以看到里面只有一个关联了 IBOutlet 和 imageView 的 StoryBoard。
使用 CoreImage 识别人脸
在开始工程中,故事板中的 imageView 组件与代码中的 IBOutlet 已关联,接下来要编写实现人脸识别的代码部分。在 ViewController.swift 文件中写下如下代码:
import UIKit
import CoreImage // 引入 CoreImage
class ViewController: UIViewController {
@IBOutlet weak var personPic: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
personPic.image = UIImage(named: “face-1”)
// 调用 detect
detect()
}
//MARK: – 识别面部
func detect() {
// 创建 personciImage 变量保存从故事板中的 UIImageView 提取图像并将其转换为 CIImage,使用 Core Image 时需要用 CIImage
guard let personciImage = CIImage(image: personPic.image!) else {
return
}
// 创建 accuracy 变量并设为 CIDetectorAccuracyHigh,可以在 CIDetectorAccuracyHigh(较强的处理能力)与 CIDetectorAccuracyLow(较弱的处理能力)中选择,因为想让准确度高一些在这里选择 CIDetectorAccuracyHigh
let accuracy = [CIDetectorAccuracy: CIDetectorAccuracyHigh]
// 这里定义了一个属于 CIDetector 类的 faceDetector 变量,并输入之前创建的 accuracy 变量
let faceDetector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: accuracy)
// 调用 faceDetector 的 featuresInImage 方法,识别器会找到所给图像中的人脸,最后返回一个人脸数组
let faces = faceDetector?.features(in: personciImage)
// 循环 faces 数组里的所有 face,并将识别到的人脸强转为 CIFaceFeature 类型
for face in faces as! [CIFaceFeature] {
print(“Found bounds are \(face.bounds)”)
// 创建名为 faceBox 的 UIView,frame 设为返回的 faces.first 的 frame,绘制一个矩形框来标识识别到的人脸
let faceBox = UIView(frame: face.bounds)
// 设置 faceBox 的边框宽度为 3
faceBox.layer.borderWidth = 3
// 设置边框颜色为红色
faceBox.layer.borderColor = UIColor.red.cgColor
// 将背景色设为 clear,意味着这个视图没有可见的背景
faceBox.backgroundColor = UIColor.clear
// 最后,把这个视图添加到 personPic imageView 上
personPic.addSubview(faceBox)
// API 不仅可以帮助你识别人脸,也可识别脸上的左右眼,我们不在图像中标识出眼睛,只是给你展示一下 CIFaceFeature 的相关属性
if face.hasLeftEyePosition {
print(“Left eye bounds are \(face.leftEyePosition)”)
}
if face.hasRightEyePosition {
print(“Right eye bounds are \(face.rightEyePosition)”)
}
}
}
}
编译并运行 app,结果应如下图所示:2.png 根据控制台的输出来看,貌似识别器识别到了人脸:Found bounds are (314.0, 243.0, 196.0, 196.0)当前的实现中没有解决的问题:
人脸识别是在原始图像上进行的,由于原始图像的分辨率比 image view 要高,因此需要设置 image view 的 content mode 为 aspect fit(保持纵横比的情况下缩放图片)。为了合适的绘制矩形框,需要计算 image view 中人脸的实际位置与尺寸还要注意的是,CoreImage 与 UIView 使用两种不同的坐标系统(看下图),因此要实现一个 CoreImage 坐标到 UIView 坐标的转换。
UIView 坐标系:
CoreImage 坐标系: 现在使用下面的代码替换 detect()方法:
func detect1() {
guard let personciImage = CIImage(image: personPic.image!) else {return}
let accuracy = [CIDetectorAccuracy: CIDetectorAccuracyHigh]
let faceDetector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: accuracy)
let faces = faceDetector?.features(in: personciImage)
// 转换坐标系
let ciImageSize = personciImage.extent.size
var transform = CGAffineTransform(scaleX: 1, y: -1)
transform = transform.translatedBy(x: 0, y: -ciImageSize.height)
for face in faces as! [CIFaceFeature] {
print(“Found bounds are \(face.bounds)”)
// 应用变换转换坐标
var faceViewBounds = face.bounds.applying(transform)
// 在图像视图中计算矩形的实际位置和大小
let viewSize = personPic.bounds.size
let scale = min(viewSize.width / ciImageSize.width, viewSize.height / ciImageSize.height)
let offsetX = (viewSize.width – ciImageSize.width * scale) / 2
let offsetY = (viewSize.height – ciImageSize.height * scale) / 2
faceViewBounds = faceViewBounds.applying(CGAffineTransform(scaleX: scale, y: scale))
faceViewBounds.origin.x += offsetX
faceViewBounds.origin.y += offsetY
let faceBox = UIView(frame: faceViewBounds)
faceBox.layer.borderWidth = 3
faceBox.layer.borderColor = UIColor.red.cgColor
faceBox.backgroundColor = UIColor.clear
personPic.addSubview(faceBox)
if face.hasLeftEyePosition {
print(“Left eye bounds are \(face.leftEyePosition)”)
}
if face.hasRightEyePosition {
print(“Right eye bounds are \(face.rightEyePosition)”)
}
}
}
上述代码中,首先使用仿射变换(AffineTransform)将 Core Image 坐标转换为 UIKit 坐标,然后编写了计算实际位置与矩形视图尺寸的代码。
再次运行 app,应该会看到人的面部周围会有一个框。OK,你已经成功使用 Core Image 识别出了人脸。但是有的童鞋在使用了上面的代码运行后可能会出现方框不存在 (即没有识别人脸) 这种情况,这是由于忘记关闭 Auto Layout 以及 Size Classes 了。选中 storyBoard 中的 ViewController,选中 view 下的 imageView。然后在右边的面板中的第一个选项卡中找到 use Auto Layout,将前面的✔️去掉就可以了
经过上面的设置后我们再次运行 App,就会看到图三出现的效果了。
构建一个人脸识别的相机应用
想象一下你有一个用来照相的相机 app,照完相后你想运行一下人脸识别来检测一下是否存在人脸。若存在一些人脸,你也许想用一些标签来对这些照片进行分类。我们不会构建一个保存照片后再处理的 app,而是一个实时的相机 app,因此需要整合一下 UIImagePicker 类,在照完相时立刻进行人脸识别。在开始工程中已经创建好了 CameraViewController 类,使用如下代码实现相机的功能:
class CameraViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
@IBOutlet var imageView: UIImageView!
let imagePicker = UIImagePickerController()
override func viewDidLoad() {
super.viewDidLoad()
imagePicker.delegate = self
}
@IBAction func takePhoto(_ sender: AnyObject) {
if !UIImagePickerController.isSourceTypeAvailable(.camera) {
return
}
imagePicker.allowsEditing = false
imagePicker.sourceType = .camera
present(imagePicker, animated: true, completion: nil)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let pickedImage = info[UIImagePickerControllerOriginalImage] as? UIImage {
imageView.contentMode = .scaleAspectFit
imageView.image = pickedImage
}
dismiss(animated: true, completion: nil)
self.detect()
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismiss(animated: true, completion: nil)
}
}
前面几行设置 UIImagePicker 委托为当前视图类,在 didFinishPickingMediaWithInfo 方法 (UIImagePicker 的委托方法) 中设置 imageView 为在方法中所选择的图像,接着返回上一视图调用 detect 函数。还没有实现 detect 函数,插入下面代码并分析一下
func detect() {
let imageOptions = NSDictionary(object: NSNumber(value: 5) as NSNumber, forKey: CIDetectorImageOrientation as NSString)
let personciImage = CIImage(cgImage: imageView.image!.cgImage!)
let accuracy = [CIDetectorAccuracy: CIDetectorAccuracyHigh]
let faceDetector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: accuracy)
let faces = faceDetector?.features(in: personciImage, options: imageOptions as? [String : AnyObject])
if let face = faces?.first as? CIFaceFeature {
print(“found bounds are \(face.bounds)”)
let alert = UIAlertController(title: “ 提示 ”, message: “ 检测到了人脸 ”, preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: “ 确定 ”, style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: nil)
if face.hasSmile {
print(“face is smiling”);
}
if face.hasLeftEyePosition {
print(“ 左眼的位置: \(face.leftEyePosition)”)
}
if face.hasRightEyePosition {
print(“ 右眼的位置: \(face.rightEyePosition)”)
}
} else {
let alert = UIAlertController(title: “ 提示 ”, message: “ 未检测到人脸 ”, preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: “ 确定 ”, style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
}
这个 detect()函数与之前实现的 detect 函数非常像,不过这次只用它来获取图像不做变换。当识别到人脸后显示一个警告信息“检测到了人脸!”,否则显示“未检测到人脸”。运行 app 测试一下:
我们已经使用到了一些 CIFaceFeature 的属性与方法,比如,若想检测人物是否微笑,可以调用.hasSmile,它会返回一个布尔值。可以分别使用.hasLeftEyePosition 与.hasRightEyePosition 检测是否存在左右眼。
同样,可以调用 hasMouthPosition 来检测是否存在嘴,若存在则可以使用 mouthPosition 属性,如下所示:
if (face.hasMouthPosition) {
print(“mouth detected”)
}
如你所见,使用 Core Image 来检测面部特征是非常简单的。除了检测嘴、笑容、眼睛外,也可以调用 leftEyeClosed 与 rightEyeClosed 检测左右眼是否睁开,这里就不在贴出代码了。总结
在这篇教程中尝试了 CoreImage 的人脸识别 API 与如何在一个相机 app 中应用它,构建了一个简单的 UIImagePicker 来选取照片并检测图像中是否存在人物。如你所见,Core Image 的人脸识别是个强大的 API!希望这篇教程能给你提供一些关于这个鲜为人知的 iOS API 有用的信息。点击 swift 版地址,OC 版地址下载最终工程, 如果觉得对您有帮助的话,请帮我点个星星哦,您的星星是对我最大的支持。(__) 嘻嘻……** 更新: 很久没有更新文章了,工作之余花时间回顾了之前写的这篇文章并运行了之前写的配套 Demo,通过打印人脸特征 CIFaceFeature 的属性(如下),发现识别的效果并不是很好,如下图:
人脸特征 CIFaceFeature 的属性
/** CIDetector 发现的脸部特征。
所有的位置都是相对于原始图像. */
NS_CLASS_AVAILABLE(10_7, 5_0)
@interface CIFaceFeature : CIFeature
{
CGRect bounds;
BOOL hasLeftEyePosition;
CGPoint leftEyePosition;
BOOL hasRightEyePosition;
CGPoint rightEyePosition;
BOOL hasMouthPosition;
CGPoint mouthPosition;
BOOL hasTrackingID;
int trackingID;
BOOL hasTrackingFrameCount;
int trackingFrameCount;
BOOL hasFaceAngle;
float faceAngle;
BOOL hasSmile;
BOOL leftEyeClosed;
BOOL rightEyeClosed;
}
/** coordinates of various cardinal points within a face.
脸部各个基点的坐标。
Note that the left eye is the eye on the left side of the face
from the observer’s perspective. It is not the left eye from
the subject’s perspective.
请注意,左眼是脸左侧的眼睛从观察者的角度来看。这不是左眼主体的视角.
*/
@property (readonly, assign) CGRect bounds; // 指示图像坐标中的人脸位置和尺寸的矩形。
@property (readonly, assign) BOOL hasLeftEyePosition; // 指示检测器是否找到了人脸的左眼。
@property (readonly, assign) CGPoint leftEyePosition; // 左眼的坐标
@property (readonly, assign) BOOL hasRightEyePosition; // 指示检测器是否找到了人脸的右眼。
@property (readonly, assign) CGPoint rightEyePosition; // 右眼的坐标
@property (readonly, assign) BOOL hasMouthPosition; // 指示检测器是否找到了人脸的嘴部
@property (readonly, assign) CGPoint mouthPosition; // 嘴部的坐标
@property (readonly, assign) BOOL hasTrackingID; // 指示面部对象是否具有跟踪 ID。
/**
* 关于 trackingID:
* coreImage 提供了在视频流中检测到的脸部的跟踪标识符,您可以使用该标识符来识别在一个视频帧中检测到的 CIFaceFeature 对象是在先前视频帧中检测到的同一个脸部。
* 只有在框架中存在人脸并且不与特定人脸相关联时,该标识符才会一直存在。如果脸部移出视频帧并在稍后返回到帧中,则分配另一个 ID。(核心图像检测面部,但不识别特定的面部。)
* 这个有点抽象
*/
@property (readonly, assign) int trackingID;
@property (readonly, assign) BOOL hasTrackingFrameCount; // 指示面部对象的布尔值具有跟踪帧计数。
@property (readonly, assign) int trackingFrameCount; // 跟踪帧计数
@property (readonly, assign) BOOL hasFaceAngle; // 指示是否有关于脸部旋转的信息可用。
@property (readonly, assign) float faceAngle; // 旋转是以度数逆时针测量的,其中零指示在眼睛之间画出的线相对于图像方向是水平的。
@property (readonly, assign) BOOL hasSmile; // 是否有笑脸
@property (readonly, assign) BOOL leftEyeClosed; // 左眼是否闭上
@property (readonly, assign) BOOL rightEyeClosed; // 右眼是否闭上
问题:那么如何让人脸识别的效果更好呢?如何让面部识别点更加精确呢?有没有别的方法呢?答案是肯定的。现在市面上有很多成熟的面部识别产品:
Face++, 收费
Video++,收费
ArcFace 虹软人脸认知引擎, 收费
百度云人脸识别, 收费
阿里云识别, 收费
等等,我们看到都是收费的。当然这些 sdk 是可以试用的。如果你有折腾精神,想自己尝试人脸识别的实现,我们可以一起交流。毕竟市面上的这些 sdk 也不是一开始就有的,也是通过人们不断研究开发出来的。而且自己折腾过程中,通过不断地遇坑爬坑,对知识的理解更加深透,自己的技术也会有增进,不是吗?不好意思,有点扯远了。Core Image 只是简单的图像识别,并不能对流中的人脸进行识别。它只适合对图片的处理。比较有名的 OpenCV(跨平台计算机视觉库)就可以用来进行面部识别,识别精度自然很高。还有就是 iOS 11.0+ 推出的 Vision 框架(让我们轻松访问苹果的模型,用于面部检测、面部特征点、文字、矩形、条形码和物体)也可以进行面部识别。后面我将会用这两个框架讲解如何进行面部识别。敬请期待!!!