程序员人生 网站导航

iOS8 Core Image In Swift:视频实时滤镜

栏目:互联网时间:2014-09-30 01:33:10

iOS8 Core Image In Swift:自动改善图像以及内置滤镜的使用

iOS8 Core Image In Swift:更复杂的滤镜

iOS8 Core Image In Swift:人脸检测以及马赛克

iOS8 Core Image In Swift:视频实时滤镜


在Core Image之前,我们虽然也能在视频录制或照片拍摄中对图像进行实时处理,但远没有Core Image使用起来方便,我们稍后会通过一个Demo回顾一下以前的做法,在此之前的例子都可以在模拟器和真机中测试,而这个例子因为会用到摄像头,所以只能在真机上测试。


视频采集

我们要进行实时滤镜的前提,就是对摄像头以及UI操作的完全控制,那么我们将不能使用系统提供的Controller,需要自己去绘制一切。
先建立一个Single View Application工程(我命名名RealTimeFilter),还是在Storyboard里关掉Auto Layout和Size Classes,然后放一个Button进去,Button的事件连到VC的openCamera方法上,然后我们给VC加两个属性:

class ViewController: UIViewController , AVCaptureVideoDataOutputSampleBufferDelegate {

    var captureSession: AVCaptureSession!

    var previewLayer: CALayer!

......

一个previewLayer用来做预览窗口,还有一个AVCaptureSession则是重点。
除此之外,我还对VC实现了AVCaptureVideoDataOutputSampleBufferDelegate协议,这个会在后面说。
要使用AV框架,必须先引入库:import AVFoundation
在viewDidLoad里实现如下:

override func viewDidLoad() {

    super.viewDidLoad()

    

    previewLayer = CALayer()

    previewLayer.bounds = CGRectMake(00self.view.frame.size.heightself.view.frame.size.width);

    previewLayer.position = CGPointMake(self.view.frame.size.width / 2.0self.view.frame.size.height / 2.0);

    previewLayer.setAffineTransform(CGAffineTransformMakeRotation(CGFloat(M_PI / 2.0)));

    

    self.view.layer.insertSublayer(previewLayer, atIndex: 0)

    

    setupCaptureSession()

}

这里先对previewLayer进行初始化,注意bounds的宽、高和设置的旋转,这是因为AVFoundation产出的图像是旋转了90度的,所以这里预先调整过来,然后把layer插到最下面,全屏显示,最后调用初始化captureSession的方法:

func setupCaptureSession() {

    captureSession = AVCaptureSession()

    captureSession.beginConfiguration()


    captureSession.sessionPreset = AVCaptureSessionPresetLow

    

    let captureDevice = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)

    

    let deviceInput = AVCaptureDeviceInput.deviceInputWithDevice(captureDevice, error: nilas 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()

}

从这个方法开始,就算正式开始了。

  1. 首先实例化一个AVCaptureSession对象,AVFoundation基于会话的概念,会话(session)被用于控制输入到输出的过程
  2. beginConfiguration与commitConfiguration总是成对调用,当后者调用的时候,会批量配置session,且是线程安全的,更重要的是,可以在session运行中执行,总是使用这对方法是一个好的习惯
  3. 然后设置它的采集质量,除了AVCaptureSessionPresetLow以外还有很多其他选项,感兴趣可以自己看看。
  4. 获取采集设备,默认的摄像设备是后置摄像头。
  5. 把上一步获取到的设备作为输入设备添加到当前session中,先用canAddInput方法判断一下是个好习惯。
  6. 添加完输入设备后再添加输出设备到session中,我在这里添加的是AVCaptureVideoDataOutput,表示视频里的每一帧,除此之外,还有AVCaptureMovieFileOutput(完整的视频)、AVCaptureAudioDataOutput(音频)、AVCaptureStillImageOutput(静态图)等。关于videoSettings属性设置,可以先看看文档说明:

    后面有写到虽然videoSettings是指定一个字典,但是目前只支持kCVPixelBufferPixelFormatTypeKey,我们用它指定像素的输出格式,这个参数直接影响到生成图像的成功与否,由于我打算先做一个实时灰度的效果,所以这里使用kCVPixelFormatType_420YpCbCr8BiPlanarFullRange的输出格式,关于这个格式的详细说明,可以看最后面的参数资料3(YUV的维基)。
  7. 后面设置了alwaysDiscardsLateVideoFrames参数,表示丢弃延迟的帧;同样用canAddInput方法判断并添加到session中。
  8. 最后设置delegate回调(AVCaptureVideoDataOutputSampleBufferDelegate协议)和回调时所处的GCD队列,并提交修改的配置。

我们现在完成一个session的建立过程,但这个session还没有开始工作,就像我们访问数据库的时候,要先打开数据库---然后建立连接---访问数据---关闭连接---关闭数据库一样,我们在openCamera方法里启动session: 

@IBAction func openCamera(sender: UIButton) {

    sender.enabled = false

    captureSession.startRunning()

}

session启动之后,不出意外的话,回调就开始了,并且是实时回调(这也是为什么要把delegate回调放在一个GCD队列中的原因),我们处理

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

    })

}

当数据缓冲区的内容更新的时候,AVFoundation就会马上调这个回调,所以我们可以在这里收集视频的每一帧,经过处理之后再渲染到layer上展示给用户。

  1. 首先这个回调给我们了一个CMSampleBufferRef类型的sampleBuffer,这是Core Media对象,我们可以通过CMSampleBufferGetImageBuffer方法把它转成Core Video对象。
  2. 然后我们把缓冲区的base地址给锁住了,锁住base地址是为了使缓冲区的内存地址变得可访问,否则在后面就取不到必需的数据,显示在layer上就只有黑屏,更详细的原因可以看这里:
    http://stackoverflow.com/questions/6468535/cvpixelbufferlockbaseaddress-why-capture-still-image-using-avfoundation
  3. 接下来从缓冲区取图像的信息,包括宽、高、每行的字节数等
  4. 因为视频的缓冲区是YUV格式的,我们要把它的luma部分提取出来
  5. 我们为了把缓冲区的图像渲染到layer上,需要用Core Graphics创建一个颜色空间和图形上下文,然后通过创建的颜色空间把缓冲区的图像渲染到上下文中
  6. cgImage就是从缓冲区创建的Core Graphics图像了(CGImage),最后我们在主线程把它赋值给layer的contents予以显示
现在在真机上编译、运行,应该能看到如下的实时灰度效果:

(这张图是通过手机截屏获取的,容易手抖,所以不是很清晰)

用Core Image处理

通过以上几步可以看到,代码不是很多,没有Core Image也能处理,但是比较费劲,难以理解、不好维护,如果想多增加一些效果(这仅仅是一个灰度效果),代码会变得非常臃肿,所以拓展性也不好。
事实上,我们想通过Core Image改造上面的代码也很简单,先从添加CIFilter和CIContext开始,这是Core Image的核心内容。
在VC上新增两个属性:

var filter: CIFilter!

lazy var context: CIContext = {

    let eaglContext = EAGLContext(API: EAGLRenderingAPI.OpenGLES2)

    let options = [kCIContextWorkingColorSpace : NSNull()]

    return CIContext(EAGLContext: eaglContext, options: options)

}()

申明一个CIFilter对象,不用实例化;懒加载一个CIContext,这个CIContext的实例通过contextWithEAGLContext:方法构造,和我们之前所使用的不一样,虽然通过contextWithOptions:方法也能构造一个GPU的CIContext,但前者的优势在于:渲染图像的过程始终在GPU上进行,并且永远不会复制回CPU存储器上,这就保证了更快的渲染速度和更好的性能。
实际上,通过contextWithOptions:创建的GPU的context,虽然渲染是在GPU上执行,但是其输出的image是不能显示的,
只有当其被复制回CPU存储器上时,才会被转成一个可被显示的image类型,比如UIImage。
我们先创建了一个EAGLContext,再通过EAGLContext创建一个CIContext,并且通过把working color space设为nil来关闭颜色管理功能,颜色管理功能会降低性能,而且只有当对颜色保真度要求很高的时候才需要颜色管理功能,在其他情况下,特别是实时处理中,颜色保真都不是特别重要(性能第一,视频帧延迟很高的app大家都不会喜欢的)。
然后我们把session的配置过程稍微修改一下,只修改一处代码即可:

kCVPixelFormatType_420YpCbCr8BiPlanarFullRange

替换为

kCVPixelFormatType_32BGRA

我们把上面那个难以理解的格式替换为BGRA像素格式,大多数情况下用此格式即可。

再把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)

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐