java 苹果 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类的接口将具有两个方法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
是视频帧应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
java 苹果 api