2018-09-04更新: 很久沒有更新文章了,工作之余花時(shí)間看了之前寫的這篇文章并運(yùn)行了之前寫的配套Demo,通過打印人臉特征CIFaceFeature的屬性,發(fā)現(xiàn)識(shí)別的效果并不是很好,具體說明見文章最底部的
更新
標(biāo)題,后續(xù)我將分別用OpenCV(跨平臺(tái)計(jì)算機(jī)視覺庫) 和 Vision (iOS 11新API)兩種庫實(shí)現(xiàn)人臉面部識(shí)別,敬請(qǐng)期待~~
OC版下載地址, swift版下載地址
CoreImage是Cocoa Touch中一個(gè)強(qiáng)大的API,也是iOS SDK中的關(guān)鍵部分,不過它經(jīng)常被忽視。在本篇教程中,我會(huì)帶大家一起驗(yàn)證CoreImage的人臉識(shí)別特性。在開始之前,我們先要簡單了解下CoreImage framework 組成
CoreImage framework組成
Apple 已經(jīng)幫我們把image的處理分類好,來看看它的結(jié)構(gòu):
主要分為三個(gè)部分:
- 定義部分:CoreImage 和CoreImageDefines。見名思義,代表了CoreImage 這個(gè)框架和它的定義。
- 操作部分:
- 濾鏡(CIFliter):CIFilter 產(chǎn)生一個(gè)CIImage。典型的,接受一到多的圖片作為輸入,經(jīng)過一些過濾操作,產(chǎn)生指定輸出的圖片。
- 檢測(cè)(CIDetector):CIDetector 檢測(cè)處理圖片的特性,如使用來檢測(cè)圖片中人臉的眼睛、嘴巴、等等。
- 特征(CIFeature):CIFeature 代表由 detector處理后產(chǎn)生的特征。
- 圖像部分:
- 畫布(CIContext):畫布類可被用與處理Quartz 2D 或者 OpenGL。可以用它來關(guān)聯(lián)CoreImage類。如濾鏡、顏色等渲染處理。
- 顏色(CIColor): 圖片的關(guān)聯(lián)與畫布、圖片像素顏色的處理。
- 向量(CIVector): 圖片的坐標(biāo)向量等幾何方法處理。
- 圖片(CIImage): 代表一個(gè)圖像,可代表關(guān)聯(lián)后輸出的圖像。
在了解上述基本知識(shí)后,我們開始通過創(chuàng)建一個(gè)工程來帶大家一步步驗(yàn)證Core Image的人臉識(shí)別特性。
將要構(gòu)建的應(yīng)用
iOS的人臉識(shí)別從iOS 5(2011)就有了,不過一直沒怎么被關(guān)注過。人臉識(shí)別API允許開發(fā)者不僅可以檢測(cè)人臉,也可以檢測(cè)到面部的一些特殊屬性,比如說微笑或眨眼。
首先,為了了解Core Image的人臉識(shí)別技術(shù)我們會(huì)創(chuàng)建一個(gè)app來識(shí)別照片中的人臉并用一個(gè)方框來標(biāo)記它。在第二個(gè)demo中,讓用戶拍攝一張照片,檢測(cè)其中的人臉并檢索人臉位置。這樣一來,就充分掌握了iOS中的人臉識(shí)別,并且學(xué)會(huì)如何利用這個(gè)強(qiáng)大卻總被忽略的API。
話不多說,開搞!
建立工程(我用的是Xcode8.0)
這里提供了初始工程,當(dāng)然你也可以自己創(chuàng)建(主要是為了方便大家)點(diǎn)我下載 用Xcode打開下載后的工程,可以看到里面只有一個(gè)關(guān)聯(lián)了IBOutlet和imageView的StoryBoard。
使用CoreImage識(shí)別人臉
在開始工程中,故事板中的imageView組件與代碼中的IBOutlet已關(guān)聯(lián),接下來要編寫實(shí)現(xiàn)人臉識(shí)別的代碼部分。在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")
// 調(diào)用detect
detect()
}
//MARK: - 識(shí)別面部
func detect() {
// 創(chuàng)建personciImage變量保存從故事板中的UIImageView提取圖像并將其轉(zhuǎn)換為CIImage,使用Core Image時(shí)需要用CIImage
guard let personciImage = CIImage(image: personPic.image!) else {
return
}
// 創(chuàng)建accuracy變量并設(shè)為CIDetectorAccuracyHigh,可以在CIDetectorAccuracyHigh(較強(qiáng)的處理能力)與CIDetectorAccuracyLow(較弱的處理能力)中選擇,因?yàn)橄胱寽?zhǔn)確度高一些在這里選擇CIDetectorAccuracyHigh
let accuracy = [CIDetectorAccuracy: CIDetectorAccuracyHigh]
// 這里定義了一個(gè)屬于CIDetector類的faceDetector變量,并輸入之前創(chuàng)建的accuracy變量
let faceDetector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: accuracy)
// 調(diào)用faceDetector的featuresInImage方法,識(shí)別器會(huì)找到所給圖像中的人臉,最后返回一個(gè)人臉數(shù)組
let faces = faceDetector?.features(in: personciImage)
// 循環(huán)faces數(shù)組里的所有face,并將識(shí)別到的人臉強(qiáng)轉(zhuǎn)為CIFaceFeature類型
for face in faces as! [CIFaceFeature] {
print("Found bounds are \(face.bounds)")
// 創(chuàng)建名為faceBox的UIView,frame設(shè)為返回的faces.first的frame,繪制一個(gè)矩形框來標(biāo)識(shí)識(shí)別到的人臉
let faceBox = UIView(frame: face.bounds)
// 設(shè)置faceBox的邊框?qū)挾葹?
faceBox.layer.borderWidth = 3
// 設(shè)置邊框顏色為紅色
faceBox.layer.borderColor = UIColor.red.cgColor
// 將背景色設(shè)為clear,意味著這個(gè)視圖沒有可見的背景
faceBox.backgroundColor = UIColor.clear
// 最后,把這個(gè)視圖添加到personPic imageView上
personPic.addSubview(faceBox)
// API不僅可以幫助你識(shí)別人臉,也可識(shí)別臉上的左右眼,我們不在圖像中標(biāo)識(shí)出眼睛,只是給你展示一下CIFaceFeature的相關(guān)屬性
if face.hasLeftEyePosition {
print("Left eye bounds are \(face.leftEyePosition)")
}
if face.hasRightEyePosition {
print("Right eye bounds are \(face.rightEyePosition)")
}
}
}
}
編譯并運(yùn)行app,結(jié)果應(yīng)如下圖所示:
Found bounds are (314.0, 243.0, 196.0, 196.0)
當(dāng)前的實(shí)現(xiàn)中沒有解決的問題:
- 人臉識(shí)別是在原始圖像上進(jìn)行的,由于原始圖像的分辨率比image view要高,因此需要設(shè)置image view的content mode為aspect fit(保持縱橫比的情況下縮放圖片)。為了合適的繪制矩形框,需要計(jì)算image view中人臉的實(shí)際位置與尺寸
- 還要注意的是,CoreImage與UIView使用兩種不同的坐標(biāo)系統(tǒng)(看下圖),因此要實(shí)現(xiàn)一個(gè)CoreImage坐標(biāo)到UIView坐標(biāo)的轉(zhuǎn)換。
UIView坐標(biāo)系:
現(xiàn)在使用下面的代碼替換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)
// 轉(zhuǎn)換坐標(biāo)系
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)")
// 應(yīng)用變換轉(zhuǎn)換坐標(biāo)
var faceViewBounds = face.bounds.applying(transform)
// 在圖像視圖中計(jì)算矩形的實(shí)際位置和大小
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坐標(biāo)轉(zhuǎn)換為UIKit坐標(biāo),然后編寫了計(jì)算實(shí)際位置與矩形視圖尺寸的代碼。
再次運(yùn)行app,應(yīng)該會(huì)看到人的面部周圍會(huì)有一個(gè)框。OK,你已經(jīng)成功使用Core Image識(shí)別出了人臉。
但是有的童鞋在使用了上面的代碼運(yùn)行后可能會(huì)出現(xiàn)方框不存在(即沒有識(shí)別人臉)這種情況,這是由于忘記關(guān)閉Auto Layout以及Size Classes了。 選中storyBoard中的ViewController,選中view下的imageView。然后在右邊的面板中的第一個(gè)選項(xiàng)卡中找到use Auto Layout ,將前面的??去掉就可以了
經(jīng)過上面的設(shè)置后我們?cè)俅芜\(yùn)行App,就會(huì)看到圖三出現(xiàn)的效果了。
構(gòu)建一個(gè)人臉識(shí)別的相機(jī)應(yīng)用
想象一下你有一個(gè)用來照相的相機(jī)app,照完相后你想運(yùn)行一下人臉識(shí)別來檢測(cè)一下是否存在人臉。若存在一些人臉,你也許想用一些標(biāo)簽來對(duì)這些照片進(jìn)行分類。我們不會(huì)構(gòu)建一個(gè)保存照片后再處理的app,而是一個(gè)實(shí)時(shí)的相機(jī)app,因此需要整合一下UIImagePicker類,在照完相時(shí)立刻進(jìn)行人臉識(shí)別。
在開始工程中已經(jīng)創(chuàng)建好了CameraViewController類,使用如下代碼實(shí)現(xiàn)相機(jī)的功能:
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)
}
}
前面幾行設(shè)置UIImagePicker委托為當(dāng)前視圖類,在didFinishPickingMediaWithInfo方法(UIImagePicker的委托方法)中設(shè)置imageView為在方法中所選擇的圖像,接著返回上一視圖調(diào)用detect函數(shù)。
還沒有實(shí)現(xiàn)detect函數(shù),插入下面代碼并分析一下:
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: "檢測(cè)到了人臉", 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: "未檢測(cè)到人臉", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "確定", style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
}
這個(gè)detect()函數(shù)與之前實(shí)現(xiàn)的detect函數(shù)非常像,不過這次只用它來獲取圖像不做變換。當(dāng)識(shí)別到人臉后顯示一個(gè)警告信息“檢測(cè)到了人臉!”,否則顯示“未檢測(cè)到人臉”。運(yùn)行app測(cè)試一下:
我們已經(jīng)使用到了一些CIFaceFeature的屬性與方法,比如,若想檢測(cè)人物是否微笑,可以調(diào)用.hasSmile,它會(huì)返回一個(gè)布爾值。可以分別使用.hasLeftEyePosition與.hasRightEyePosition檢測(cè)是否存在左右眼。
同樣,可以調(diào)用hasMouthPosition來檢測(cè)是否存在嘴,若存在則可以使用mouthPosition屬性,如下所示:
if (face.hasMouthPosition) {
print("mouth detected")
}
如你所見,使用Core Image來檢測(cè)面部特征是非常簡單的。除了檢測(cè)嘴、笑容、眼睛外,也可以調(diào)用leftEyeClosed與rightEyeClosed檢測(cè)左右眼是否睜開,這里就不在貼出代碼了。
總結(jié)
在這篇教程中嘗試了CoreImage的人臉識(shí)別API與如何在一個(gè)相機(jī)app中應(yīng)用它,構(gòu)建了一個(gè)簡單的UIImagePicker來選取照片并檢測(cè)圖像中是否存在人物。
如你所見,Core Image的人臉識(shí)別是個(gè)強(qiáng)大的API!希望這篇教程能給你提供一些關(guān)于這個(gè)鮮為人知的iOS API有用的信息。
點(diǎn)擊swift版地址,OC版地址下載最終工程, 如果覺得對(duì)您有幫助的話,請(qǐng)幫我點(diǎn)個(gè)星星哦,您的星星是對(duì)我最大的支持。(__) 嘻嘻……**
更新:
很久沒有更新文章了,工作之余花時(shí)間回顧了之前寫的這篇文章并運(yùn)行了之前寫的配套Demo,通過打印人臉特征CIFaceFeature的屬性(如下),發(fā)現(xiàn)識(shí)別的效果并不是很好,如下圖:
人臉特征CIFaceFeature的屬性
/** CIDetector發(fā)現(xiàn)的臉部特征。
所有的位置都是相對(duì)于原始圖像. */
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.
臉部各個(gè)基點(diǎn)的坐標(biāo)。
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.
請(qǐng)注意,左眼是臉左側(cè)的眼睛從觀察者的角度來看。 這不是左眼主體的視角.
*/
@property (readonly, assign) CGRect bounds; // 指示圖像坐標(biāo)中的人臉位置和尺寸的矩形。
@property (readonly, assign) BOOL hasLeftEyePosition; // 指示檢測(cè)器是否找到了人臉的左眼。
@property (readonly, assign) CGPoint leftEyePosition; // 左眼的坐標(biāo)
@property (readonly, assign) BOOL hasRightEyePosition; // 指示檢測(cè)器是否找到了人臉的右眼。
@property (readonly, assign) CGPoint rightEyePosition; // 右眼的坐標(biāo)
@property (readonly, assign) BOOL hasMouthPosition; // 指示檢測(cè)器是否找到了人臉的嘴部
@property (readonly, assign) CGPoint mouthPosition; // 嘴部的坐標(biāo)
@property (readonly, assign) BOOL hasTrackingID; // 指示面部對(duì)象是否具有跟蹤ID。
/**
* 關(guān)于trackingID:
* coreImage提供了在視頻流中檢測(cè)到的臉部的跟蹤標(biāo)識(shí)符,您可以使用該標(biāo)識(shí)符來識(shí)別在一個(gè)視頻幀中檢測(cè)到的CIFaceFeature對(duì)象是在先前視頻幀中檢測(cè)到的同一個(gè)臉部。
* 只有在框架中存在人臉并且不與特定人臉相關(guān)聯(lián)時(shí),該標(biāo)識(shí)符才會(huì)一直存在。如果臉部移出視頻幀并在稍后返回到幀中,則分配另一個(gè)ID。 (核心圖像檢測(cè)面部,但不識(shí)別特定的面部。)
* 這個(gè)有點(diǎn)抽象
*/
@property (readonly, assign) int trackingID;
@property (readonly, assign) BOOL hasTrackingFrameCount; // 指示面部對(duì)象的布爾值具有跟蹤幀計(jì)數(shù)。
@property (readonly, assign) int trackingFrameCount; // 跟蹤幀計(jì)數(shù)
@property (readonly, assign) BOOL hasFaceAngle; // 指示是否有關(guān)于臉部旋轉(zhuǎn)的信息可用。
@property (readonly, assign) float faceAngle; // 旋轉(zhuǎn)是以度數(shù)逆時(shí)針測(cè)量的,其中零指示在眼睛之間畫出的線相對(duì)于圖像方向是水平的。
@property (readonly, assign) BOOL hasSmile; // 是否有笑臉
@property (readonly, assign) BOOL leftEyeClosed; // 左眼是否閉上
@property (readonly, assign) BOOL rightEyeClosed; // 右眼是否閉上
問題:那么如何讓人臉識(shí)別的效果更好呢? 如何讓面部識(shí)別點(diǎn)更加精確呢?有沒有別的方法呢? 答案是肯定的。
現(xiàn)在市面上有很多成熟的面部識(shí)別產(chǎn)品:
- Face++, 收費(fèi)
- Video++,收費(fèi)
- ArcFace 虹軟人臉認(rèn)知引擎, 收費(fèi)
- 百度云人臉識(shí)別, 收費(fèi)
- 阿里云識(shí)別, 收費(fèi)
等等, 我們看到都是收費(fèi)的。 當(dāng)然這些sdk是可以試用的。如果你有折騰精神,想自己嘗試人臉識(shí)別的實(shí)現(xiàn),我們可以一起交流。 畢竟市面上的這些sdk也不是一開始就有的, 也是通過人們不斷研究開發(fā)出來的。 而且自己折騰過程中,通過不斷地遇坑爬坑,對(duì)知識(shí)的理解更加深透,自己的技術(shù)也會(huì)有增進(jìn),不是嗎? 不好意思,有點(diǎn)扯遠(yuǎn)了。
Core Image只是簡單的圖像識(shí)別, 并不能對(duì)流中的人臉進(jìn)行識(shí)別。 它只適合對(duì)圖片的處理。比較有名的OpenCV(跨平臺(tái)計(jì)算機(jī)視覺庫)就可以用來進(jìn)行面部識(shí)別,識(shí)別精度自然很高。還有就是iOS 11.0+ 推出的Vision框架(讓我們輕松訪問蘋果的模型,用于面部檢測(cè)、面部特征點(diǎn)、文字、矩形、條形碼和物體)也可以進(jìn)行面部識(shí)別。后面我將會(huì)用這兩個(gè)框架講解如何進(jìn)行面部識(shí)別。敬請(qǐng)期待!!!