在你的脸上! 找出苹果的面部检测API

我正在制作具有面部检测功能的本地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类的接口将具有两个方法startCapturingstopCapturing 。 注意这两种操作方法。 当用户按下按钮时,视频捕获将开始,而当用户抬起按钮时,视频捕获将停止。 像Snapchat,Instragram,Vine或其他视频捕获应用程序。 您可以在我的代码中签出情节提要,但可以随时创建自己的界面来启动和停止视频捕获。

UIViewController类中的viewDidLoaddidReceiveMemoryWarning方法将被覆盖。 这些将用于实例化我们的视频捕获对象,并在有内存警告时停止捕获它。

进入情节提要,然后选择视图控制器。 在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方法中讨论devicefaceDetector对象。 我已经创建了startCapturingstopCapturing方法,但尚未实现它们调用的所有方法。 我们将VideoCaptureDevice所有这些内容,然后实现VideoCaptureDeviceFaceDetector类。

届会

当您想从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对象, videoRectpreviewRect 。 一旦有了一个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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值