iOS8 Core Image In Swift:视频实时滤镜
iOS8 Core Image In Swift:自己主动改善图像以及内置滤镜的使用
iOS8 Core Image In Swift:更复杂的滤镜
iOS8 Core Image In Swift:人脸检測以及马赛克
iOS8 Core Image In Swift:视频实时滤镜
视频採集
class ViewController: UIViewController , AVCaptureVideoDataOutputSampleBufferDelegate {
var captureSession: AVCaptureSession!
var previewLayer: CALayer!
......
除此之外,我还对VC实现了AVCaptureVideoDataOutputSampleBufferDelegate协议,这个会在后面说。 要使用AV框架,必须先引入库:import AVFoundation |
override func viewDidLoad() {
super.viewDidLoad()
previewLayer = CALayer()
previewLayer.bounds = CGRectMake(0, 0, self.view.frame.size.height, self.view.frame.size.width);
previewLayer.position = CGPointMake(self.view.frame.size.width / 2.0, self.view.frame.size.height / 2.0);
previewLayer.setAffineTransform(CGAffineTransformMakeRotation(CGFloat(M_PI / 2.0)));
self.view.layer.insertSublayer(previewLayer, atIndex: 0)
setupCaptureSession()
}
func setupCaptureSession() {
captureSession = AVCaptureSession()
captureSession.beginConfiguration()
captureSession.sessionPreset = AVCaptureSessionPresetLow
let captureDevice = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
let deviceInput = AVCaptureDeviceInput.deviceInputWithDevice(captureDevice, error: nil) as AVCaptureDeviceInput
if captureSession.canAddInput(deviceInput) {
captureSession.addInput(deviceInput)
}
let dataOutput = AVCaptureVideoDataOutput()
dataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey : kCVPixelFormatType_420YpCbCr8BiPlanarFullRange]
dataOutput.alwaysDiscardsLateVideoFrames = true
if captureSession.canAddOutput(dataOutput) {
captureSession.addOutput(dataOutput)
}
let queue = dispatch_queue_create("VideoQueue", DISPATCH_QUEUE_SERIAL)
dataOutput.setSampleBufferDelegate(self, queue: queue)
captureSession.commitConfiguration()
}
从这种方法開始,就算正式開始了。
- 首先实例化一个AVCaptureSession对象,AVFoundation基于会话的概念,会话(session)被用于控制输入到输出的过程
- beginConfiguration与commitConfiguration总是成对调用,当后者调用的时候,会批量配置session,且是线程安全的,更重要的是,能够在session执行中执行,总是使用这对方法是一个好的习惯
- 然后设置它的採集质量。除了AVCaptureSessionPresetLow以外还有非常多其它选项,感兴趣能够自己看看。
- 获取採集设备,默认的摄像设备是后置摄像头。
- 把上一步获取到的设备作为输入设备加入到当前session中。先用canAddInput方法推断一下是个好习惯。
- 加入完输入设备后再加入输出设备到session中,我在这里加入的是AVCaptureVideoDataOutput。表示视频里的每一帧。除此之外,还有AVCaptureMovieFileOutput(完整的视频)、AVCaptureAudioDataOutput(音频)、AVCaptureStillImageOutput(静态图)等。关于videoSettings属性设置,能够先看看文档说明:
后面有写到尽管videoSettings是指定一个字典,可是眼下仅仅支持kCVPixelBufferPixelFormatTypeKey,我们用它指定像素的输出格式,这个參数直接影响到生成图像的成功与否,由于我打算先做一个实时灰度的效果,所以这里使用kCVPixelFormatType_420YpCbCr8BiPlanarFullRange的输出格式。关于这个格式的具体说明,能够看最后面的參数资料3(YUV的维基)。 - 后面设置了alwaysDiscardsLateVideoFrames參数,表示丢弃延迟的帧;相同用canAddInput方法推断并加入到session中。
- 最后设置delegate回调(AVCaptureVideoDataOutputSampleBufferDelegate协议)和回调时所处的GCD队列。并提交改动的配置。
我们如今完毕一个session的建立过程,但这个session还没有開始工作,就像我们訪问数据库的时候,要先打开数据库---然后建立连接---訪问数据---关闭连接---关闭数据库一样。我们在openCamera方法里启动session:
@IBAction func openCamera(sender: UIButton) {
sender.enabled = false
captureSession.startRunning()
}
optional func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!)
这个回调就能够了:
Core Image之前的方式
func captureOutput(captureOutput: AVCaptureOutput!,
didOutputSampleBuffer sampleBuffer: CMSampleBuffer!,
fromConnection connection: AVCaptureConnection!) {
let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
CVPixelBufferLockBaseAddress(imageBuffer, 0)
let width = CVPixelBufferGetWidthOfPlane(imageBuffer, 0)
let height = CVPixelBufferGetHeightOfPlane(imageBuffer, 0)
let bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0)
let lumaBuffer = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0)
let grayColorSpace = CGColorSpaceCreateDeviceGray()
let context = CGBitmapContextCreate(lumaBuffer, width, height, 8, bytesPerRow, grayColorSpace, CGBitmapInfo.allZeros)
let cgImage = CGBitmapContextCreateImage(context)
dispatch_sync(dispatch_get_main_queue(), {
self.previewLayer.contents = cgImage
})
}
- 首先这个回调给我们了一个CMSampleBufferRef类型的sampleBuffer。这是Core Media对象。我们能够通过CMSampleBufferGetImageBuffer方法把它转成Core Video对象。
- 然后我们把缓冲区的base地址给锁住了,锁住base地址是为了使缓冲区的内存地址变得可訪问。否则在后面就取不到必需的数据,显示在layer上就仅仅有黑屏。更具体的原因能够看这里:
http://stackoverflow.com/questions/6468535/cvpixelbufferlockbaseaddress-why-capture-still-image-using-avfoundation - 接下来从缓冲区取图像的信息,包括宽、高、每行的字节数等
- 由于视频的缓冲区是YUV格式的。我们要把它的luma部分提取出来
- 我们为了把缓冲区的图像渲染到layer上。须要用Core Graphics创建一个颜色空间和图形上下文,然后通过创建的颜色空间把缓冲区的图像渲染到上下文中
- cgImage就是从缓冲区创建的Core Graphics图像了(CGImage),最后我们在主线程把它赋值给layer的contents予以显示
用Core Image处理
var filter: CIFilter!
lazy var context: CIContext = {
let eaglContext = EAGLContext(API: EAGLRenderingAPI.OpenGLES2)
let options = [kCIContextWorkingColorSpace : NSNull()]
return CIContext(EAGLContext: eaglContext, options: options)
}()
实际上。通过contextWithOptions:创建的GPU的context。尽管渲染是在GPU上执行,可是其输出的image是不能显示的。 仅仅有当其被复制回CPU存储器上时,才会被转成一个可被显示的image类型,比方UIImage。 |
kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
替换为
kCVPixelFormatType_32BGRA
再把session的回调进行一些改动,变成我们熟悉的方式,就像这样:
func captureOutput(captureOutput: AVCaptureOutput!,
didOutputSampleBuffer sampleBuffer: CMSampleBuffer!,
fromConnection connection: AVCaptureConnection!) {
let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
// CVPixelBufferLockBaseAddress(imageBuffer, 0)
// let width = CVPixelBufferGetWidthOfPlane(imageBuffer, 0)
// let height = CVPixelBufferGetHeightOfPlane(imageBuffer, 0)
// let bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0)
// let lumaBuffer = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0)
//
// let grayColorSpace = CGColorSpaceCreateDeviceGray()
// let context = CGBitmapContextCreate(lumaBuffer, width, height, 8, bytesPerRow, grayColorSpace, CGBitmapInfo.allZeros)
// let cgImage = CGBitmapContextCreateImage(context)
var outputImage = CIImage(CVPixelBuffer: imageBuffer)
if filter != nil {
filter.setValue(outputImage, forKey: kCIInputImageKey)
outputImage = filter.outputImage
}
let cgImage = context.createCGImage(outputImage, fromRect: outputImage.extent())
dispatch_sync(dispatch_get_main_queue(), {
self.previewLayer.contents = cgImage
})
}
这是一段拓展性、维护性都比較好的代码了:
- 先拿到缓冲区,看从缓冲区直接取到一张CIImage
- 假设指定了滤镜,就应用到图像上;反之则显示原图
- 通过context创建CGImage的实例
- 在主队列中显示到layer上
class ViewController: UIViewController , AVCaptureVideoDataOutputSampleBufferDelegate {
@IBOutlet var filterButtonsContainer: UIView!
var captureSession: AVCaptureSession!
var previewLayer: CALayer!
var filter: CIFilter!
lazy var context: CIContext = {
let eaglContext = EAGLContext(API: EAGLRenderingAPI.OpenGLES2)
let options = [kCIContextWorkingColorSpace : NSNull()]
return CIContext(EAGLContext: eaglContext, options: options)
}()
lazy var filterNames: [String] = {
return ["CIColorInvert","CIPhotoEffectMono","CIPhotoEffectInstant","CIPhotoEffectTransfer"]
}()
......
......
filterButtonsContainer.hidden = true
......
@IBAction func openCamera(sender: UIButton) {
sender.enabled = false
captureSession.startRunning()
self.filterButtonsContainer.hidden = false
}
@IBAction func applyFilter(sender: UIButton) {
var filterName = filterNames[sender.tag]
filter = CIFilter(name: filterName)
}
保存到图库
......
previewLayer = CALayer()
// previewLayer.bounds = CGRectMake(0, 0, self.view.frame.size.height, self.view.frame.size.width);
// previewLayer.position = CGPointMake(self.view.frame.size.width / 2.0, self.view.frame.size.height / 2.0);
// previewLayer.setAffineTransform(CGAffineTransformMakeRotation(CGFloat(M_PI / 2.0)));
previewLayer.anchorPoint = CGPointZero
previewLayer.bounds = view.bounds
......
如今你执行的话看到的将是方向不对的图像。
然后我们把方向统一放到captureSession的回调中处理。改动之前写的实现:
......
var outputImage = CIImage(CVPixelBuffer: imageBuffer)
let orientation = UIDevice.currentDevice().orientation
var t: CGAffineTransform!
if orientation == UIDeviceOrientation.Portrait {
t = CGAffineTransformMakeRotation(CGFloat(-M_PI / 2.0))
} else if orientation == UIDeviceOrientation.PortraitUpsideDown {
t = CGAffineTransformMakeRotation(CGFloat(M_PI / 2.0))
} else if (orientation == UIDeviceOrientation.LandscapeRight) {
t = CGAffineTransformMakeRotation(CGFloat(M_PI))
} else {
t = CGAffineTransformMakeRotation(0)
}
outputImage = outputImage.imageByApplyingTransform(t)
if filter != nil {
filter.setValue(outputImage, forKey: kCIInputImageKey)
outputImage = filter.outputImage
}
......
执行之后看到的效果和之前就一样了。
方向处理完后我们还要用一个实例变量保存这个outputImage,由于这里面含有图像的元数据。我们不会丢弃它:
给VC加入一个CIImage的属性:
var ciImage: CIImage!
......
if filter != nil {
filter.setValue(outputImage, forKey: kCIInputImageKey)
outputImage = filter.outputImage
}
let cgImage = context.createCGImage(outputImage, fromRect: outputImage.extent())
ciImage = outputImage
......
最后是takePicture的方法实现:
@IBAction func takePicture(sender: UIButton) {
sender.enabled = false
captureSession.stopRunning()
var cgImage = context.createCGImage(ciImage, fromRect: ciImage.extent())
ALAssetsLibrary().writeImageToSavedPhotosAlbum(cgImage, metadata: ciImage.properties())
{ (url: NSURL!, error :NSError!) -> Void in
if error == nil {
println("保存成功")
println(url)
} else {
let alert = UIAlertView(title: "错误",
message: error.localizedDescription,
delegate: nil,
cancelButtonTitle: "确定")
alert.show()
}
self.captureSession.startRunning()
sender.enabled = true
}
}
这里须要导入AssetsLibrary库:import AssetsLibrary。writeImageToSavedPhotosAlbum方法的回调 block用到了跟随闭包语法。 |
在真机上编译、执行看看吧。
注:由于我是用layer来做预览容器的,它没有autoresizingMask这种属性。你会发现横屏的时候就显示不正常了,在iOS 8gh,你能够通过重写VC的下面方法来兼容横屏:
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator
coordinator: UIViewControllerTransitionCoordinator) {
previewLayer.bounds.size = size
}
录制视频
前期配置
......
// Video Records
@IBOutlet var recordsButton: UIButton!
var assetWriter: AVAssetWriter?
var assetWriterPixelBufferInput: AVAssetWriterInputPixelBufferAdaptor?
var isWriting = false
var currentSampleTime: CMTime?
var currentVideoDimensions: CMVideoDimensions?
......
- recordsButton,为了方便的获取录制按钮的实例而添加的属性
- assetWriter,这是一个AVAssetWriter对象的实例。这个类的工作方式非常像AVCaptureSession,也是为了控制输入输出的流程而存在的
- assetWriterPixelBufferInput。一个AVAssetWriterInputPixelBufferAdaptor对象,这个属性的作用如同它的名字,它同意我们不断地添加像素缓冲区到assetWriter对象里
- isWriting。假设我们当前正在录制视频。则会用这个实例变量记录下来
- currentSampleTime,这是一个时间戳,在AVFoundation框架里,每一块加入的数据(视频或音频等)除了data部分外,还须要一个当前的时间,每一帧的时间都不同。这就形成了每一帧的持续时间(时间间隔)
- currentVideoDimensions,这个属性描写叙述了视频尺寸,尽管这个属性并不重要,可是我更加懒得把尺寸写死。它的单位是像素
func movieURL() -> NSURL {
var tempDir = NSTemporaryDirectory()
let urlString = tempDir.stringByAppendingPathComponent("tmpMov.mov")
return NSURL(fileURLWithPath: urlString)
}
func checkForAndDeleteFile() {
let fm = NSFileManager.defaultManager()
var url = movieURL()
let exist = fm.fileExistsAtPath(movieURL().path!)
var error: NSError?
if exist {
fm.removeItemAtURL(movieURL(), error: &error)
println("删除之前的暂时文件")
if let errorDescription = error?.localizedDescription {
println(errorDescription)
}
}
}
。
func createWriter() {
self.checkForAndDeleteFile()
var error: NSError?
assetWriter = AVAssetWriter(URL: movieURL(), fileType: AVFileTypeQuickTimeMovie, error: &error)
if let errorDescription = error?.localizedDescription {
println("创建writer失败")
println(errorDescription)
return
}
let outputSettings = [
AVVideoCodecKey : AVVideoCodecH264,
AVVideoWidthKey : Int(currentVideoDimensions!.width),
AVVideoHeightKey : Int(currentVideoDimensions!.height)
]
let assetWriterVideoInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: outputSettings)
assetWriterVideoInput.expectsMediaDataInRealTime = true
assetWriterVideoInput.transform = CGAffineTransformMakeRotation(CGFloat(M_PI / 2.0))
let sourcePixelBufferAttributesDictionary = [
kCVPixelBufferPixelFormatTypeKey : kCVPixelFormatType_32BGRA,
kCVPixelBufferWidthKey : Int(currentVideoDimensions!.width),
kCVPixelBufferHeightKey : Int(currentVideoDimensions!.height),
kCVPixelFormatOpenGLESCompatibility : kCFBooleanTrue
]
assetWriterPixelBufferInput = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: assetWriterVideoInput,
sourcePixelBufferAttributes: sourcePixelBufferAttributesDictionary)
if assetWriter!.canAddInput(assetWriterVideoInput) {
assetWriter!.addInput(assetWriterVideoInput)
} else {
println("不能加入视频writer的input \(assetWriterVideoInput)")
}
}
- 首先检查了文件是否存在,假设存在的话就删除旧的暂时文件,不然AVAssetWriter会因无法写入文件而报错
- 实例化一个AVAssetWriter对象,把须要写的文件URL和文件类型传递给它。再给它一个存储错误信息的指针,方便在出错的时候排查
- 创建一个outputSettings的字典应用到AVAssetWriterInput对象上。这个对象之前没有提到,但也是相当重要的一个对象。它表示了一个输入设备。比方视频、音频的输入等,不同的设备拥有不同的參数和配置,并不复杂。我们这里就不考虑音频输入了。
在这个视频的配置里。我们配置了视频的编码。以及用获取到的当前视频设备尺寸(单位像素)初始化了宽、高
- 设置expectsMediaDataInRealTime为true。这是从摄像头捕获的源中进行实时编码的必要參数
- 设置了视频的transform,主要也是为了解决方向问题
- 创建另外一个属性字典去实例化一个AVAssetWriterInputPixelBufferAdaptor对象。我们在视频採集的过程中。会不断地通过这个缓冲区往AVAssetWriter对象里加入内容,实例化的參数中还有AVAssetWriterInput对象,属性字典标识了缓冲区的大小与格式。
- 最后推断一下是否能加入这个输入设备,尽管大多数情况下推断一定为真。并且为假的情况我们也没办法考虑了。但预先推断还是一个好的编码习惯
处理每一帧
由于之前在captureOutput:didOutputSampleBuffer:这个回调方法中。我们是先对图像的方向进行处理,然后再对其应用滤镜,而录制视频的时候我们不须要对方向进行处理。由于在配置AVAssetWriterInput对象的时候我们已经处理过了。所以我们先将应用滤镜和方向调整的代码互换一下。变成先应用滤镜,再处理方向,然后在他们中间插入处理录制视频的代码:
......
if self.filter != nil {
self.filter.setValue(outputImage, forKey: kCIInputImageKey)
outputImage = self.filter.outputImage
}
// 处理录制视频
let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer)
self.currentVideoDimensions = CMVideoFormatDescriptionGetDimensions(formatDescription)
self.currentSampleTime = CMSampleBufferGetOutputPresentationTimeStamp(sampleBuffer)
if self.isWriting {
if self.assetWriterPixelBufferInput?.assetWriterInput.readyForMoreMediaData == true {
var newPixelBuffer: Unmanaged<CVPixelBuffer>? = nil
CVPixelBufferPoolCreatePixelBuffer(nil, self.assetWriterPixelBufferInput?.pixelBufferPool, &newPixelBuffer)
self.context.render(outputImage,
toCVPixelBuffer: newPixelBuffer?
.takeUnretainedValue(),
bounds: outputImage.extent(),
colorSpace: nil)
let success = self.assetWriterPixelBufferInput?.appendPixelBuffer(newPixelBuffer?
.takeUnretainedValue(),
withPresentationTime: self.currentSampleTime!)
newPixelBuffer?.autorelease()
if success == false {
println("Pixel Buffer没有append成功")
}
}
}
let orientation = UIDevice.currentDevice().orientation
var t: CGAffineTransform!
......
- 获取尺寸和时间,这两个值在后面会用到。
强调一下,时间这个參数是非常重要的,当你有一系列的帧的时候,assetWriter必须知道何时显示他们。我们除了通过CMSampleBufferGetOutputPresentationTimeStamp函数获取之外。也能够手动创建一个时间,比方把每一个缓冲区的时间设置为比上一个缓冲区时间多1/30秒。这就相当于创建一个每秒30帧的视频。可是这不能保证视频时序的真实情况,由于某些滤镜(或者其它操作)可能会耗时过长
- 当前是否须要录制视频。录制视频事实上就是写文件的一个过程
- 推断assetWriter是否已经准备好输入数据了
- 一切都准备好后,我们就先配置一个缓冲区。用CVPixelBufferPoolCreatePixelBuffer函数能创建基于池的缓冲区,它的优点是在创建缓冲区的时候会把之前对assetWriterPixelBufferInput对象的配置项应用到新的缓冲区上。这样就避免了你又一次对新的缓冲区进行配置。有一点须要注意,假设我们的assetWriter还未開始工作,那么当我们调用assetWriterPixelBufferInput的pixelBufferPool时候会得到一个空指针,缓冲区当然也就创建不了了
- 我们把缓冲区准备好后,就利用context把图像渲染到里面
- 把缓冲区写入到暂时文件里。同一时候得到是否写入成功的返回值
- 由于在Swift里CVPixelBufferPoolCreatePixelBuffer函数须要的是一个手动管理引用计数的对象(Unmanaged对象),所以须要自己把它处理一下
- 假设第6步失败的话就输出一下
autoreleasepool {
// ....
}
保存视频到图库
@IBAction func record() {
if isWriting {
self.isWriting = false
assetWriterPixelBufferInput = nil
recordsButton.enabled = false
assetWriter?
.finishWritingWithCompletionHandler({[unowned self] () -> Void in
println("录制完毕")
self.recordsButton.setTitle("处理中...", forState: UIControlState.Normal)
self.saveMovieToCameraRoll()
})
} else {
createWriter()
recordsButton.setTitle("停止录制...", forState: UIControlState.Normal)
assetWriter?.startWriting()
assetWriter?.startSessionAtSourceTime(currentSampleTime!)
isWriting = true
}
}
假设不调用这两个方法,在appendPixelBuffer的时候就会有问题。就算最后能保存,也仅仅能得到一个空的视频文件。
func saveMovieToCameraRoll() {
ALAssetsLibrary().writeVideoAtPathToSavedPhotosAlbum(movieURL(), completionBlock: { (url: NSURL!, error: NSError?
) -> Void in
if let errorDescription = error?.localizedDescription {
println("写入视频错误:\(errorDescription)")
} else {
self.checkForAndDeleteFile()
println("写入视频成功")
}
self.recordsButton.enabled = true
self.recordsButton.setTitle("開始录制", forState: UIControlState.Normal)
})
}
保存成功后就能够删除暂时文件了。
编译、执行吧:
局部滤镜
不同于上一篇,AVFoundation框架内置了检測人脸的功能,所以我们不须要使用CIDetector。
标记人脸
// 标记人脸
var faceLayer: CALayer?
改动setupCaptureSession方法,在captureSession调用commitConfiguration方法之前加入下面代码:
......
// 为了检測人脸
let metadataOutput = AVCaptureMetadataOutput()
metadataOutput.setMetadataObjectsDelegate(self, queue: dispatch_get_main_queue())
if captureSession.canAddOutput(metadataOutput) {
captureSession.addOutput(metadataOutput)
println(metadataOutput.availableMetadataObjectTypes)
metadataOutput.metadataObjectTypes = [AVMetadataObjectTypeFace]
}
......
由于我们要在回调中直接操作Layer的显示,所以我把回调放在主队列中。
// MARK: - AVCaptureMetadataOutputObjectsDelegate
func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!) {
// println(metadataObjects)
if metadataObjects.count > 0 {
//识别到的第一张脸
var faceObject = metadataObjects.first as AVMetadataFaceObject
if faceLayer == nil {
faceLayer = CALayer()
faceLayer?
.borderColor = UIColor.redColor().CGColor
faceLayer?
.borderWidth = 1
view.layer.addSublayer(faceLayer)
}
let faceBounds = faceObject.bounds
let viewSize = view.bounds.size
faceLayer?
.position = CGPoint(x: viewSize.width * (1 - faceBounds.origin.y - faceBounds.size.height / 2),
y: viewSize.height * (faceBounds.origin.x + faceBounds.size.width / 2))
faceLayer?.bounds.size = CGSize(width: faceBounds.size.width * viewSize.height,
height: faceBounds.size.height * viewSize.width)
print(faceBounds.origin)
print("###")
print(faceLayer!.position)
print("###")
print(faceLayer!.bounds)
}
}
- 參数中的metadataObjects数组就是AVFoundation框架给我们的关于图像的全部元数据,由于我仅仅设置了须要人脸检測。所以简单推断是否为空后,取出当中的数据就可以。
在这里我仅仅对第一张脸进行了处理
- 接下来初始化Layer,并设置边框
- 取到的faceObject对象尽管包括了bounds属性,但并不能直接使用,由于从AVFoundation视频中取到的bounds,是一个0~1之间的数,是相对于图像的百分比,所以我们在设置position时,做了两步:把x、y颠倒。修正方向等问题。我仅仅是简单地适配了Portrait方向,此处能达到目的就可以。再和view的宽、高相乘,事实上是和Layer的父Layer的宽、高相乘。
- 设置size也如上
使用滤镜
......
// 标记人脸
// var faceLayer: CALayer?
var faceObject: AVMetadataFaceObject?
......
// MARK: - AVCaptureMetadataOutputObjectsDelegate
func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!) {
// println(metadataObjects)
if metadataObjects.count > 0 {
//识别到的第一张脸
faceObject = metadataObjects.first as?
AVMetadataFaceObject
/*
if faceLayer == nil {
faceLayer = CALayer()
faceLayer?.borderColor = UIColor.redColor().CGColor
faceLayer?.borderWidth = 1
view.layer.addSublayer(faceLayer)
}
let faceBounds = faceObject.bounds
let viewSize = view.bounds.size
faceLayer?.position = CGPoint(x: viewSize.width * (1 - faceBounds.origin.y - faceBounds.size.height / 2),
y: viewSize.height * (faceBounds.origin.x + faceBounds.size.width / 2))
faceLayer?.bounds.size = CGSize(width: faceBounds.size.height * viewSize.width,
height: faceBounds.size.width * viewSize.height)
print(faceBounds.origin)
print("###")
print(faceLayer!.position)
print("###")
print(faceLayer!.bounds)
*/
}
}
......
if self.filter != nil { // 之前做的全局滤镜
self.filter.setValue(outputImage, forKey: kCIInputImageKey)
outputImage = self.filter.outputImage
}
if self.faceObject != nil { // 脸部处理
outputImage = self.makeFaceWithCIImage(outputImage, faceObject: self.faceObject!)
}
......
func makeFaceWithCIImage(inputImage: CIImage, faceObject: AVMetadataFaceObject) -> CIImage {
var filter = CIFilter(name: "CIPixellate")
filter.setValue(inputImage, forKey: kCIInputImageKey)
// 1.
filter.setValue(max(inputImage.extent().size.width, inputImage.extent().size.height) / 60, forKey: kCIInputScaleKey)
let fullPixellatedImage = filter.outputImage
var maskImage: CIImage!
let faceBounds = faceObject.bounds
// 2.
let centerX = inputImage.extent().size.width * (faceBounds.origin.x + faceBounds.size.width / 2)
let centerY = inputImage.extent().size.height * (1 - faceBounds.origin.y - faceBounds.size.height / 2)
let radius = faceBounds.size.width * inputImage.extent().size.width / 2
let radialGradient = CIFilter(name: "CIRadialGradient",
withInputParameters: [
"inputRadius0" : radius,
"inputRadius1" : radius + 1,
"inputColor0" : CIColor(red: 0, green: 1, blue: 0, alpha: 1),
"inputColor1" : CIColor(red: 0, green: 0, blue: 0, alpha: 0),
kCIInputCenterKey : CIVector(x: centerX, y: centerY)
])
let radialGradientOutputImage = radialGradient.outputImage.imageByCroppingToRect(inputImage.extent())
if maskImage == nil {
maskImage = radialGradientOutputImage
} else {
println(radialGradientOutputImage)
maskImage = CIFilter(name: "CISourceOverCompositing",
withInputParameters: [
kCIInputImageKey : radialGradientOutputImage,
kCIInputBackgroundImageKey : maskImage
]).outputImage
}
let blendFilter = CIFilter(name: "CIBlendWithMask")
blendFilter.setValue(fullPixellatedImage, forKey: kCIInputImageKey)
blendFilter.setValue(inputImage, forKey: kCIInputBackgroundImageKey)
blendFilter.setValue(maskImage, forKey: kCIInputMaskImageKey)
return blendFilter.outputImage
}
- 把马赛克的效果变大,kCIInputScaleKey默认值为0.5,你能够把这行代码凝视掉后看效果
- 计算脸部的中心点和半径,计算方法和之前didOutputMetadataObjects这个delegate回调中的计算方法一样。复制过来就可以了
GitHub下载地址
我在GitHub上会保持更新。參考资料:
1. http://weblog.invasivecode.com/post/18445861158/a-very-cool-custom-video-camera-with
3. http://en.wikipedia.org/wiki/YUV