当前位置:   article > 正文

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

didoutputsamplebuffer

我正在制作具有面部检测功能的本地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类

  1. import Foundation
  2. import UIKit
  3. class VideoCaptureController: UIViewController {
  4. var videoCapture: VideoCapture?
  5. override func viewDidLoad() {
  6. videoCapture = VideoCapture()
  7. }
  8. override func didReceiveMemoryWarning() {
  9. stopCapturing()
  10. }
  11. func startCapturing() {
  12. do {
  13. try videoCapture!.startCapturing(self.view)
  14. }
  15. catch {
  16. // Error
  17. }
  18. }
  19. func stopCapturing() {
  20. videoCapture!.stopCapturing()
  21. }
  22. @IBAction func touchDown(sender: AnyObject) {
  23. let button = sender as! UIButton
  24. button.setTitle("Stop", forState: UIControlState.Normal)
  25. startCapturing()
  26. }
  27. @IBAction func touchUp(sender: AnyObject) {
  28. let button = sender as! UIButton
  29. button.setTitle("Start", forState: UIControlState.Normal)
  30. stopCapturing()
  31. }
  32. }

捕获视频并执行面部检测的魔术将封装在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类

  1. import Foundation
  2. import AVFoundation
  3. import UIKit
  4. import CoreMotion
  5. import ImageIO
  6. class VideoCapture: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
  7. var isCapturing: Bool = false
  8. var session: AVCaptureSession?
  9. var device: AVCaptureDevice?
  10. var input: AVCaptureInput?
  11. var preview: CALayer?
  12. var faceDetector: FaceDetector?
  13. var dataOutput: AVCaptureVideoDataOutput?
  14. var dataOutputQueue: dispatch_queue_t?
  15. var previewView: UIView?
  16. enum VideoCaptureError: ErrorType {
  17. case SessionPresetNotAvailable
  18. case InputDeviceNotAvailable
  19. case InputCouldNotBeAddedToSession
  20. case DataOutputCouldNotBeAddedToSession
  21. }
  22. override init() {
  23. super.init()
  24. device = VideoCaptureDevice.create()
  25. faceDetector = FaceDetector()
  26. }
  27. func startCapturing(previewView: UIView) throws {
  28. isCapturing = true
  29. self.previewView = previewView
  30. self.session = AVCaptureSession()
  31. try setSessionPreset()
  32. try setDeviceInput()
  33. try addInputToSession()
  34. setDataOutput()
  35. try addDataOutputToSession()
  36. addPreviewToView(self.previewView!)
  37. session!.startRunning()
  38. }
  39. func stopCapturing() {
  40. isCapturing = false
  41. stopSession()
  42. removePreviewFromView()
  43. removeFeatureViews()
  44. preview = nil
  45. dataOutput = nil
  46. dataOutputQueue = nil
  47. session = nil
  48. previewView = nil
  49. }
  50. }

此类需要从NSObject继承,因为AVCaptureVideoDataOutputSampleBufferDelegate协议是从NSObjectProtocol继承的。 NSObject负责实现NSObjectProtocol 。 稍后我们可以讨论为AVCaptureVideoDataOutputSampleBufferDelegate实现captureOutput

我创建了一个枚举,以便此类的用户可以捕获特定的错误。 稍后,我将在覆盖的init方法中讨论devicefaceDetector对象。 我已经创建了startCapturingstopCapturing方法,但尚未实现它们调用的所有方法。 我们将VideoCaptureDevice所有这些内容,然后实现VideoCaptureDeviceFaceDetector类。

届会

当您想从iOS设备的摄像头捕获视频或图像时,需要实例化AVCaptureSession类。 我们在startCapturing方法中分配变量session 。 然后,我们调用setSessionPreset方法。 这应该添加到您的VideoCapture类中。

  1. private func setSessionPreset() throws {
  2. if (session!.canSetSessionPreset(AVCaptureSessionPreset640x480)) {
  3. session!.sessionPreset = AVCaptureSessionPreset640x480
  4. }
  5. else {
  6. throw VideoCaptureError.SessionPresetNotAvailable
  7. }
  8. }

这会检查相机是否可以捕获640×480分辨率的视频。 如果不是,则将引发错误。 我使用的是640×480的分辨率,但是您可以使用其他分辨率。 这是它们的列表 。 现在有了AVCaptureSession对象,我们可以开始添加输入和输出类了。

输入设备

我们将向VideoCapture类添加两个函数。 setDeviceInput方法实例化AVCaptureDeviceInput类。 这将处理输入设备的端口,并允许您在iOS设备上使用相机。

  1. private func setDeviceInput() throws {
  2. do {
  3. self.input = try AVCaptureDeviceInput(device: self.device)
  4. }
  5. catch {
  6. throw VideoCaptureError.InputDeviceNotAvailable
  7. }
  8. }
  9. private func addInputToSession() throws {
  10. if (session!.canAddInput(self.input)) {
  11. session!.addInput(self.input)
  12. }
  13. else {
  14. throw VideoCaptureError.InputCouldNotBeAddedToSession
  15. }
  16. }

数据输出

我们有会话和输入类,但是现在我们想捕获视频帧以进行面部检测。 我们将向VideoCapture类添加另一个方法。

  1. private func setDataOutput() {
  2. self.dataOutput = AVCaptureVideoDataOutput()
  3. var videoSettings = [NSObject : AnyObject]()
  4. videoSettings[kCVPixelBufferPixelFormatTypeKey] = Int(CInt(kCVPixelFormatType_32BGRA))
  5. self.dataOutput!.videoSettings = videoSettings
  6. self.dataOutput!.alwaysDiscardsLateVideoFrames = true
  7. self.dataOutputQueue = dispatch_queue_create("VideoDataOutputQueue", DISPATCH_QUEUE_SERIAL)
  8. self.dataOutput!.setSampleBufferDelegate(self, queue: self.dataOutputQueue!)
  9. }

AVCaptureVideoDataOutput类将使我们能够处理来自视频提要的未压缩帧。 videoSettings属性是具有一个键/值对的字典。 kCVPixelBufferPixelFormatTypeKey是视频帧应返回的格式类型。它是一个四个字符的代码,对于AVCaptureVideoDataOutput类,该代码将转换为整数。

可以想象,相机将产生许多视频帧。 甚至可能是60 /秒。 这就是为什么我们使用dispatch_queue_create方法通过Grand Central Dispatch创建一个串行队列的原因。 这种队列将一次将一个请求按添加到队列的顺序处理。

最后,为队列创建一个示例缓冲区。 如果我们花太多时间处理帧,则将请求从队列中删除,因为我们将属性alwaysDiscardsLateVideoFrames标记为true。

接下来,将此方法添加到您的VideoCapture类。

  1. private func addDataOutputToSession() throws {
  2. if (self.session!.canAddOutput(self.dataOutput!)) {
  3. self.session!.addOutput(self.dataOutput!)
  4. }
  5. else {
  6. throw VideoCaptureError.DataOutputCouldNotBeAddedToSession
  7. }
  8. }

这会将AVCaptureVideoDataOutput类添加到AVCaptureSession类。

眼见为实

将以下方法添加到您的VideoCapture类。

  1. private func addPreviewToView(view: UIView) {
  2. self.preview = AVCaptureVideoPreviewLayer(session: session!)
  3. self.preview!.frame = view.bounds
  4. view.layer.addSublayer(self.preview!)
  5. }

我们将实例化AVCaptureVideoPreviewLayer类。 通过该课程,您可以查看来自输入设备的视频帧。 然后,我们将其添加为从VideoCaptureController传入的视图的子层。 在我们的示例中,它是与该控制器关联的主UIView。 您会注意到,我们将图层的框架设置为包围视图的边界。 基本上,它是视图的完整大小。

如果您在VideoCapture类中检出了startCapturing方法,您将看到所有方法均已就位。 但是,我们绝对还没有完成。 我们如何从队列中接收视频帧,以及如何实际检测这些帧中的面部?

AVCaptureVideoDataOutputSampleBufferDelegate协议

AVCaptureVideoDataOutputSampleBufferDelegate协议仅需要一种方法。 它是captureOutput

  1. func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!) {
  2. let image = getImageFromBuffer(sampleBuffer)
  3. let features = getFacialFeaturesFromImage(image)
  4. let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer)
  5. let cleanAperture = CMVideoFormatDescriptionGetCleanAperture(formatDescription!, false)
  6. dispatch_async(dispatch_get_main_queue()) {
  7. self.alterPreview(features, cleanAperture: cleanAperture)
  8. }
  9. }

我们可以从传递给该函数的CMSampleBuffer抓取视频帧。 我们获取有关图像的一些属性,然后通过Grand Central Dispatch调度异步请求。 我们使用主线程dispatch_get_main_queue 。 您想在更新应用程序的UI时使用主线程,因为其他请求不会在请求之前发生,从而导致错误。

让我们将函数getImageFromBuffer添加到VideoCapture类中。

  1. private func getImageFromBuffer(buffer: CMSampleBuffer) -> CIImage {
  2. let pixelBuffer = CMSampleBufferGetImageBuffer(buffer)
  3. let attachments = CMCopyDictionaryOfAttachments(kCFAllocatorDefault, buffer, kCMAttachmentMode_ShouldPropagate)
  4. let image = CIImage(CVPixelBuffer: pixelBuffer!, options: attachments as? [String : AnyObject])
  5. return image
  6. }

CMSampleBufferGetImageBuffer将返回图像缓冲区。 attachments字典由CMCopyDictionaryOfAttachments填充,该模板复制了sampleBuffer对象的所有属性。 返回一个Core Image对象。

继续,并将getFacialFeaturesFromImage方法添加到您的VideoCapture类。 人脸检测代码封装在FaceDetector类中。 我们稍后再讲。

  1. private func getFacialFeaturesFromImage(image: CIImage) -> [CIFeature] {
  2. let imageOptions = [CIDetectorImageOrientation : 6]
  3. return self.faceDetector!.getFacialFeaturesFromImage(image, options: imageOptions)
  4. }

我已将检测方向设置为纵向(即6),因为此应用已锁定为纵向模式。 FaceDetector类上的getFacialFeaturesFromImage方法返回CIFeature对象的数组。 在我们的例子中,它们将是CIFaceFeature的子类。 该对象可以告诉您是否可以看到眼睛和嘴巴以及它们的位置。 它甚至会告诉您是否检测到微笑。 在创建FaceDetector类之前,让我们看一下我们异步调度以与ui交互的alterPreview方法。

  1. private func alterPreview(features: [CIFeature], cleanAperture: CGRect) {
  2. removeFeatureViews()
  3. if (features.count == 0 || cleanAperture == CGRect.zero || !isCapturing) {
  4. return
  5. }
  6. for feature in features {
  7. let faceFeature = feature as? CIFaceFeature
  8. if (faceFeature!.hasLeftEyePosition) {
  9. addEyeViewToPreview(faceFeature!.leftEyePosition.x, yPosition: faceFeature!.leftEyePosition.y, cleanAperture: cleanAperture)
  10. }
  11. if (faceFeature!.hasRightEyePosition) {
  12. addEyeViewToPreview(faceFeature!.rightEyePosition.x, yPosition: faceFeature!.rightEyePosition.y, cleanAperture: cleanAperture)
  13. }
  14. }
  15. }
  16. private func removeFeatureViews() {
  17. if let pv = previewView {
  18. for view in pv.subviews {
  19. if (view.tag == 1001) {
  20. view.removeFromSuperview()
  21. }
  22. }
  23. }
  24. }
  25. private func addEyeViewToPreview(xPosition: CGFloat, yPosition: CGFloat, cleanAperture: CGRect) {
  26. let eyeView = getFeatureView()
  27. let isMirrored = preview!.contentsAreFlipped()
  28. let previewBox = preview!.frame
  29. previewView!.addSubview(eyeView)
  30. var eyeFrame = transformFacialFeaturePosition(xPosition, yPosition: yPosition, videoRect: cleanAperture, previewRect: previewBox, isMirrored: isMirrored)
  31. eyeFrame.origin.x -= 37
  32. eyeFrame.origin.y -= 37
  33. eyeView.frame = eyeFrame
  34. }

alterPreview方法中,我们将定位在眼睛上方的视图移除,因为我们将它们重新定位在每一帧上。 如果没有发现面部特征,那么我们将不对框架做任何事情而保释。 如果找到左眼或右眼,则调用addEyeViewToPreview(xPosition方法。此方法包含一些我们还需要添加到VideoCapture类中的方法getFeatureView将加载一个Storyboard文件,我在其中将其命名为HeartView我的项目。

  1. private func getFeatureView() -> UIView {
  2. let heartView = NSBundle.mainBundle().loadNibNamed("HeartView", owner: self, options: nil)[0] as? UIView
  3. heartView!.backgroundColor = UIColor.clearColor()
  4. heartView!.layer.removeAllAnimations()
  5. heartView!.tag = 1001
  6. return heartView!
  7. }
  8. private func transformFacialFeaturePosition(xPosition: CGFloat, yPosition: CGFloat, videoRect: CGRect, previewRect: CGRect, isMirrored: Bool) -> CGRect {
  9. var featureRect = CGRect(origin: CGPoint(x: xPosition, y: yPosition), size: CGSize(width: 0, height: 0))
  10. let widthScale = previewRect.size.width / videoRect.size.height
  11. let heightScale = previewRect.size.height / videoRect.size.width
  12. let transform = isMirrored ? CGAffineTransformMake(0, heightScale, -widthScale, 0, previewRect.size.width, 0) :
  13. CGAffineTransformMake(0, heightScale, widthScale, 0, 0, 0)
  14. featureRect = CGRectApplyAffineTransform(featureRect, transform)
  15. featureRect = CGRectOffset(featureRect, previewRect.origin.x, previewRect.origin.y)
  16. return featureRect
  17. }

getFeatureView方法加载XIB文件,并用1001整数进行标记,以便我们稍后返回并使用removeFeatureViews方法轻松删除它。 transformFacialFeaturePosition方法使用CGRectApplyAffineTransform将坐标从一个坐标系转换到另一个坐标系。 您为什么要这样做? 视频以640×480的分辨率捕获,但是我们的预览视图处于纵向模式,宽度和高度不同,具体取决于视图如何渲染以适合窗口。 不同的视图由CGRect对象, videoRectpreviewRect 。 一旦有了一个CGRect对象,该对象表示预览视图坐标系中的眼睛位置,就可以将它们作为子视图附加到previewView

我们的VideoCapture类看起来不错。 我们可以通过创建剩下的两个类来完成:VideoCaptureDevice类和FaceDetector类。

VideoCaptureDevice类

在VideoCapture组中创建一个名为VideoCaptureDevice.swift的新文件。 这是GitHub上的完整类。 现在,在VideoCapture类中具有setDeviceInput方法的设备对象。

  1. import Foundation
  2. import AVFoundation
  3. class VideoCaptureDevice {
  4. static func create() -> AVCaptureDevice {
  5. var device: AVCaptureDevice?
  6. AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo).forEach { videoDevice in
  7. if (videoDevice.position == AVCaptureDevicePosition.Front) {
  8. device = videoDevice as? AVCaptureDevice
  9. }
  10. }
  11. if (nil == device) {
  12. device = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
  13. }
  14. return device!
  15. }
  16. }

此类包含一个静态工厂方法,该方法创建AVCaptureDevice的实例。 该应用程序使用视频,因此我们使用devicesWithMediaType方法来查找AVMediaTypeVideo类型的设备数组。 由于我们正在进行人脸检测,因此我认为前置摄像头将是理想的选择。 如果未找到前置摄像头,则使用defaultDeviceWithMediaType方法返回可以拍摄视频的摄像头,该摄像头很可能是后置摄像头。

FaceDetector类别

将一个名为FaceDetector.swift的文件添加到VideoCapture组。 这是GitHub上的完整类。

  1. import Foundation
  2. import CoreImage
  3. import AVFoundation
  4. class FaceDetector {
  5. var detector: CIDetector?
  6. var options: [String : AnyObject]?
  7. var context: CIContext?
  8. init() {
  9. context = CIContext()
  10. options = [String : AnyObject]()
  11. options![CIDetectorAccuracy] = CIDetectorAccuracyLow
  12. detector = CIDetector(ofType: CIDetectorTypeFace, context: context!, options: options!)
  13. }
  14. func getFacialFeaturesFromImage(image: CIImage, options: [String : AnyObject]) -> [CIFeature] {
  15. return self.detector!.featuresInImage(image, options: options)
  16. }
  17. }

CIDetector类是我们将用来检测从sampleBuffer创建的CIImage对象中的面Kong的接口。 CIDetectorTypeFace参数是一个字符串,它将告诉CIDetector类搜索面Kong。 CIDetector的选项之一是CIDetectorAccuracy 。 我们将其设置为低,这样就不会在要处理的帧数上看到性能问题。

最后的想法

请记住,iOS模拟器没有相机,因此您需要使用真实设备测试该应用程序。 当您运行该应用并开始捕获时,您应该会看到故事板文件叠加在您的眼睛上。 将来,我想改进此代码,以便不必为每个框架创建新的UIView对象。 视频捕获完成后,它只会移动现有的或删除它们。

就是这样! 如果您有任何疑问或意见,请告诉我!

翻译自: https://www.javacodegeeks.com/2016/05/face-figuring-apples-face-detection-api.html

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/229411
推荐阅读
相关标签
  

闽ICP备14008679号