我正在制作具有面部检测功能的本地iOS应用。 苹果有一个很棒的图像检测API,可以在图像或视频帧中找到人脸,条形码甚至矩形形状。 该API随iOS 5.0一起发布,但我认为Swift 2.2和Xcode 7.3的更新示例有望对人们有所帮助。
位于https://github.com/dcandre/face-it上的代码将允许您从iOS设备的摄像头查看视频供稿,并将Storyboard文件叠加在左眼和右眼位置的预览视图之上。
我将假设您可以在Xcode中创建一个Single View Application。 我的代码将设备方向限制为纵向模式。 我在项目浏览器中创建了一个名为Video-Capture
。 在该组中,您可以创建文件VideoCaptureController.swift
。
VideoCaptureController类
- import Foundation
- import UIKit
-
- class VideoCaptureController: UIViewController {
- var videoCapture: VideoCapture?
-
- override func viewDidLoad() {
- videoCapture = VideoCapture()
- }
-
- override func didReceiveMemoryWarning() {
- stopCapturing()
- }
-
- func startCapturing() {
- do {
- try videoCapture!.startCapturing(self.view)
- }
- catch {
- // Error
- }
- }
-
- func stopCapturing() {
- videoCapture!.stopCapturing()
- }
-
- @IBAction func touchDown(sender: AnyObject) {
- let button = sender as! UIButton
- button.setTitle("Stop", forState: UIControlState.Normal)
-
- startCapturing()
- }
-
- @IBAction func touchUp(sender: AnyObject) {
- let button = sender as! UIButton
- button.setTitle("Start", forState: UIControlState.Normal)
-
- stopCapturing()
- }
- }
捕获视频并执行面部检测的魔术将封装在VideoCapture
类中,我们将在其下创建。 现在,我们假定VideoCapture类的接口将具有两个方法startCapturing
和stopCapturing
。 注意这两种操作方法。 当用户按下按钮时,视频捕获将开始,而当用户抬起按钮时,视频捕获将停止。 像Snapchat,Instragram,Vine或其他视频捕获应用程序。 您可以在我的代码中签出情节提要,但可以随时创建自己的界面来启动和停止视频捕获。
UIViewController类中的viewDidLoad
和didReceiveMemoryWarning
方法将被覆盖。 这些将用于实例化我们的视频捕获对象,并在有内存警告时停止捕获它。
进入情节提要,然后选择视图控制器。 在Identity Inspector中,将自定义类更改为VideoCaptureController
文件。 我使用了Touch Down,Touch Up Inside和Touch Up Outside事件来附加到视图控制器的操作方法。
在讨论VideoCapture类之前,我想总结一下Apple的视频捕获过程。 要从iOS设备的相机捕获图像或视频,请使用AVFoundation框架。 AVCaptureSession类将输入(例如照相机)和输出(例如保存到图像文件)耦合。 我们将使用名为AVCaptureVideoDataOutput的输出。 这将捕获视频中的帧,并允许我们看到摄像机看到的内容。
继续,在VideoCapture组中创建一个名为VideoCapture.swift
的文件。 完整的VideoCapture类可以在GitHub上找到 。 这是类声明:
VideoCapture类
- import Foundation
- import AVFoundation
- import UIKit
- import CoreMotion
- import ImageIO
-
- class VideoCapture: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
- var isCapturing: Bool = false
- var session: AVCaptureSession?
- var device: AVCaptureDevice?
- var input: AVCaptureInput?
- var preview: CALayer?
- var faceDetector: FaceDetector?
- var dataOutput: AVCaptureVideoDataOutput?
- var dataOutputQueue: dispatch_queue_t?
- var previewView: UIView?
-
- enum VideoCaptureError: ErrorType {
- case SessionPresetNotAvailable
- case InputDeviceNotAvailable
- case InputCouldNotBeAddedToSession
- case DataOutputCouldNotBeAddedToSession
- }
-
- override init() {
- super.init()
-
- device = VideoCaptureDevice.create()
-
- faceDetector = FaceDetector()
- }
-
- func startCapturing(previewView: UIView) throws {
- isCapturing = true
-
- self.previewView = previewView
-
- self.session = AVCaptureSession()
-
- try setSessionPreset()
-
- try setDeviceInput()
-
- try addInputToSession()
-
- setDataOutput()
-
- try addDataOutputToSession()
-
- addPreviewToView(self.previewView!)
-
- session!.startRunning()
- }
-
- func stopCapturing() {
- isCapturing = false
-
- stopSession()
-
- removePreviewFromView()
-
- removeFeatureViews()
-
- preview = nil
- dataOutput = nil
- dataOutputQueue = nil
- session = nil
- previewView = nil
- }
- }
此类需要从NSObject
继承,因为AVCaptureVideoDataOutputSampleBufferDelegate协议是从NSObjectProtocol继承的。 NSObject负责实现NSObjectProtocol
。 稍后我们可以讨论为AVCaptureVideoDataOutputSampleBufferDelegate
实现captureOutput
。
我创建了一个枚举,以便此类的用户可以捕获特定的错误。 稍后,我将在覆盖的init方法中讨论device
和faceDetector
对象。 我已经创建了startCapturing
和stopCapturing
方法,但尚未实现它们调用的所有方法。 我们将VideoCaptureDevice
所有这些内容,然后实现VideoCaptureDevice
和FaceDetector
类。
届会
当您想从iOS设备的摄像头捕获视频或图像时,需要实例化AVCaptureSession类。 我们在startCapturing
方法中分配变量session
。 然后,我们调用setSessionPreset
方法。 这应该添加到您的VideoCapture类中。
- private func setSessionPreset() throws {
- if (session!.canSetSessionPreset(AVCaptureSessionPreset640x480)) {
- session!.sessionPreset = AVCaptureSessionPreset640x480
- }
- else {
- throw VideoCaptureError.SessionPresetNotAvailable
- }
- }
这会检查相机是否可以捕获640×480分辨率的视频。 如果不是,则将引发错误。 我使用的是640×480的分辨率,但是您可以使用其他分辨率。 这是它们的列表 。 现在有了AVCaptureSession对象,我们可以开始添加输入和输出类了。
输入设备
我们将向VideoCapture类添加两个函数。 setDeviceInput
方法实例化AVCaptureDeviceInput类。 这将处理输入设备的端口,并允许您在iOS设备上使用相机。
- private func setDeviceInput() throws {
- do {
- self.input = try AVCaptureDeviceInput(device: self.device)
- }
- catch {
- throw VideoCaptureError.InputDeviceNotAvailable
- }
- }
-
- private func addInputToSession() throws {
- if (session!.canAddInput(self.input)) {
- session!.addInput(self.input)
- }
- else {
- throw VideoCaptureError.InputCouldNotBeAddedToSession
- }
- }
数据输出
我们有会话和输入类,但是现在我们想捕获视频帧以进行面部检测。 我们将向VideoCapture类添加另一个方法。
- private func setDataOutput() {
- self.dataOutput = AVCaptureVideoDataOutput()
-
- var videoSettings = [NSObject : AnyObject]()
- videoSettings[kCVPixelBufferPixelFormatTypeKey] = Int(CInt(kCVPixelFormatType_32BGRA))
-
- self.dataOutput!.videoSettings = videoSettings
- self.dataOutput!.alwaysDiscardsLateVideoFrames = true
-
- self.dataOutputQueue = dispatch_queue_create("VideoDataOutputQueue", DISPATCH_QUEUE_SERIAL)
-
- self.dataOutput!.setSampleBufferDelegate(self, queue: self.dataOutputQueue!)
- }
AVCaptureVideoDataOutput类将使我们能够处理来自视频提要的未压缩帧。 videoSettings
属性是具有一个键/值对的字典。 kCVPixelBufferPixelFormatTypeKey
是视频帧应返回的格式类型。它是一个四个字符的代码,对于AVCaptureVideoDataOutput
类,该代码将转换为整数。
可以想象,相机将产生许多视频帧。 甚至可能是60 /秒。 这就是为什么我们使用dispatch_queue_create
方法通过Grand Central Dispatch创建一个串行队列的原因。 这种队列将一次将一个请求按添加到队列的顺序处理。
最后,为队列创建一个示例缓冲区。 如果我们花太多时间处理帧,则将请求从队列中删除,因为我们将属性alwaysDiscardsLateVideoFrames
标记为true。
接下来,将此方法添加到您的VideoCapture类。
- private func addDataOutputToSession() throws {
- if (self.session!.canAddOutput(self.dataOutput!)) {
- self.session!.addOutput(self.dataOutput!)
- }
- else {
- throw VideoCaptureError.DataOutputCouldNotBeAddedToSession
- }
- }
这会将AVCaptureVideoDataOutput类添加到AVCaptureSession类。
眼见为实
将以下方法添加到您的VideoCapture类。
- private func addPreviewToView(view: UIView) {
- self.preview = AVCaptureVideoPreviewLayer(session: session!)
- self.preview!.frame = view.bounds
-
- view.layer.addSublayer(self.preview!)
- }
我们将实例化AVCaptureVideoPreviewLayer类。 通过该课程,您可以查看来自输入设备的视频帧。 然后,我们将其添加为从VideoCaptureController传入的视图的子层。 在我们的示例中,它是与该控制器关联的主UIView。 您会注意到,我们将图层的框架设置为包围视图的边界。 基本上,它是视图的完整大小。
如果您在VideoCapture类中检出了startCapturing
方法,您将看到所有方法均已就位。 但是,我们绝对还没有完成。 我们如何从队列中接收视频帧,以及如何实际检测这些帧中的面部?
AVCaptureVideoDataOutputSampleBufferDelegate协议
AVCaptureVideoDataOutputSampleBufferDelegate协议仅需要一种方法。 它是captureOutput
。
- func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!) {
-
- let image = getImageFromBuffer(sampleBuffer)
-
- let features = getFacialFeaturesFromImage(image)
-
- let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer)
-
- let cleanAperture = CMVideoFormatDescriptionGetCleanAperture(formatDescription!, false)
-
- dispatch_async(dispatch_get_main_queue()) {
- self.alterPreview(features, cleanAperture: cleanAperture)
- }
- }
我们可以从传递给该函数的CMSampleBuffer抓取视频帧。 我们获取有关图像的一些属性,然后通过Grand Central Dispatch调度异步请求。 我们使用主线程dispatch_get_main_queue 。 您想在更新应用程序的UI时使用主线程,因为其他请求不会在请求之前发生,从而导致错误。
让我们将函数getImageFromBuffer
添加到VideoCapture类中。
- private func getImageFromBuffer(buffer: CMSampleBuffer) -> CIImage {
- let pixelBuffer = CMSampleBufferGetImageBuffer(buffer)
-
- let attachments = CMCopyDictionaryOfAttachments(kCFAllocatorDefault, buffer, kCMAttachmentMode_ShouldPropagate)
-
- let image = CIImage(CVPixelBuffer: pixelBuffer!, options: attachments as? [String : AnyObject])
-
- return image
- }
CMSampleBufferGetImageBuffer将返回图像缓冲区。 attachments
字典由CMCopyDictionaryOfAttachments填充,该模板复制了sampleBuffer
对象的所有属性。 返回一个Core Image对象。
继续,并将getFacialFeaturesFromImage
方法添加到您的VideoCapture类。 人脸检测代码封装在FaceDetector类中。 我们稍后再讲。
- private func getFacialFeaturesFromImage(image: CIImage) -> [CIFeature] {
- let imageOptions = [CIDetectorImageOrientation : 6]
-
- return self.faceDetector!.getFacialFeaturesFromImage(image, options: imageOptions)
- }
我已将检测方向设置为纵向(即6),因为此应用已锁定为纵向模式。 FaceDetector类上的getFacialFeaturesFromImage
方法返回CIFeature对象的数组。 在我们的例子中,它们将是CIFaceFeature的子类。 该对象可以告诉您是否可以看到眼睛和嘴巴以及它们的位置。 它甚至会告诉您是否检测到微笑。 在创建FaceDetector类之前,让我们看一下我们异步调度以与ui交互的alterPreview
方法。
- private func alterPreview(features: [CIFeature], cleanAperture: CGRect) {
- removeFeatureViews()
-
- if (features.count == 0 || cleanAperture == CGRect.zero || !isCapturing) {
- return
- }
-
- for feature in features {
- let faceFeature = feature as? CIFaceFeature
-
- if (faceFeature!.hasLeftEyePosition) {
-
- addEyeViewToPreview(faceFeature!.leftEyePosition.x, yPosition: faceFeature!.leftEyePosition.y, cleanAperture: cleanAperture)
- }
-
- if (faceFeature!.hasRightEyePosition) {
-
- addEyeViewToPreview(faceFeature!.rightEyePosition.x, yPosition: faceFeature!.rightEyePosition.y, cleanAperture: cleanAperture)
- }
-
- }
-
- }
-
- private func removeFeatureViews() {
- if let pv = previewView {
- for view in pv.subviews {
- if (view.tag == 1001) {
- view.removeFromSuperview()
- }
- }
- }
- }
-
- private func addEyeViewToPreview(xPosition: CGFloat, yPosition: CGFloat, cleanAperture: CGRect) {
- let eyeView = getFeatureView()
- let isMirrored = preview!.contentsAreFlipped()
- let previewBox = preview!.frame
-
- previewView!.addSubview(eyeView)
-
- var eyeFrame = transformFacialFeaturePosition(xPosition, yPosition: yPosition, videoRect: cleanAperture, previewRect: previewBox, isMirrored: isMirrored)
-
- eyeFrame.origin.x -= 37
- eyeFrame.origin.y -= 37
-
- eyeView.frame = eyeFrame
- }
在alterPreview
方法中,我们将定位在眼睛上方的视图移除,因为我们将它们重新定位在每一帧上。 如果没有发现面部特征,那么我们将不对框架做任何事情而保释。 如果找到左眼或右眼,则调用addEyeViewToPreview(xPosition
方法。此方法包含一些我们还需要添加到VideoCapture类中的方法getFeatureView
将加载一个Storyboard文件,我在其中将其命名为HeartView
我的项目。
- private func getFeatureView() -> UIView {
- let heartView = NSBundle.mainBundle().loadNibNamed("HeartView", owner: self, options: nil)[0] as? UIView
- heartView!.backgroundColor = UIColor.clearColor()
- heartView!.layer.removeAllAnimations()
- heartView!.tag = 1001
-
- return heartView!
- }
-
- private func transformFacialFeaturePosition(xPosition: CGFloat, yPosition: CGFloat, videoRect: CGRect, previewRect: CGRect, isMirrored: Bool) -> CGRect {
-
- var featureRect = CGRect(origin: CGPoint(x: xPosition, y: yPosition), size: CGSize(width: 0, height: 0))
- let widthScale = previewRect.size.width / videoRect.size.height
- let heightScale = previewRect.size.height / videoRect.size.width
-
- let transform = isMirrored ? CGAffineTransformMake(0, heightScale, -widthScale, 0, previewRect.size.width, 0) :
- CGAffineTransformMake(0, heightScale, widthScale, 0, 0, 0)
-
- featureRect = CGRectApplyAffineTransform(featureRect, transform)
-
- featureRect = CGRectOffset(featureRect, previewRect.origin.x, previewRect.origin.y)
-
- return featureRect
- }
getFeatureView
方法加载XIB文件,并用1001整数进行标记,以便我们稍后返回并使用removeFeatureViews
方法轻松删除它。 transformFacialFeaturePosition
方法使用CGRectApplyAffineTransform将坐标从一个坐标系转换到另一个坐标系。 您为什么要这样做? 视频以640×480的分辨率捕获,但是我们的预览视图处于纵向模式,宽度和高度不同,具体取决于视图如何渲染以适合窗口。 不同的视图由CGRect对象, videoRect
和previewRect
。 一旦有了一个CGRect对象,该对象表示预览视图坐标系中的眼睛位置,就可以将它们作为子视图附加到previewView
。
我们的VideoCapture类看起来不错。 我们可以通过创建剩下的两个类来完成:VideoCaptureDevice类和FaceDetector类。
VideoCaptureDevice类
在VideoCapture组中创建一个名为VideoCaptureDevice.swift
的新文件。 这是GitHub上的完整类。 现在,在VideoCapture类中具有setDeviceInput
方法的设备对象。
- import Foundation
- import AVFoundation
-
- class VideoCaptureDevice {
-
- static func create() -> AVCaptureDevice {
- var device: AVCaptureDevice?
-
- AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo).forEach { videoDevice in
- if (videoDevice.position == AVCaptureDevicePosition.Front) {
- device = videoDevice as? AVCaptureDevice
- }
- }
-
- if (nil == device) {
- device = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
- }
-
- return device!
- }
- }
此类包含一个静态工厂方法,该方法创建AVCaptureDevice的实例。 该应用程序使用视频,因此我们使用devicesWithMediaType
方法来查找AVMediaTypeVideo类型的设备数组。 由于我们正在进行人脸检测,因此我认为前置摄像头将是理想的选择。 如果未找到前置摄像头,则使用defaultDeviceWithMediaType
方法返回可以拍摄视频的摄像头,该摄像头很可能是后置摄像头。
FaceDetector类别
将一个名为FaceDetector.swift
的文件添加到VideoCapture组。 这是GitHub上的完整类。
- import Foundation
- import CoreImage
- import AVFoundation
-
- class FaceDetector {
- var detector: CIDetector?
- var options: [String : AnyObject]?
- var context: CIContext?
-
- init() {
- context = CIContext()
-
- options = [String : AnyObject]()
- options![CIDetectorAccuracy] = CIDetectorAccuracyLow
-
- detector = CIDetector(ofType: CIDetectorTypeFace, context: context!, options: options!)
- }
-
- func getFacialFeaturesFromImage(image: CIImage, options: [String : AnyObject]) -> [CIFeature] {
- return self.detector!.featuresInImage(image, options: options)
- }
- }
CIDetector类是我们将用来检测从sampleBuffer创建的CIImage对象中的面Kong的接口。 CIDetectorTypeFace
参数是一个字符串,它将告诉CIDetector
类搜索面Kong。 CIDetector的选项之一是CIDetectorAccuracy 。 我们将其设置为低,这样就不会在要处理的帧数上看到性能问题。
最后的想法
请记住,iOS模拟器没有相机,因此您需要使用真实设备测试该应用程序。 当您运行该应用并开始捕获时,您应该会看到故事板文件叠加在您的眼睛上。 将来,我想改进此代码,以便不必为每个框架创建新的UIView对象。 视频捕获完成后,它只会移动现有的或删除它们。
就是这样! 如果您有任何疑问或意见,请告诉我!
翻译自: https://www.javacodegeeks.com/2016/05/face-figuring-apples-face-detection-api.html