• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

iOS8CoreImageInSwift:视频实时滤镜

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

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 Applicationproject(我命名名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改造上面的代码也非常easy,先从加入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)

    // 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

    })

}

这是一段拓展性、维护性都比較好的代码了:

  1. 先拿到缓冲区,看从缓冲区直接取到一张CIImage
  2. 假设指定了滤镜,就应用到图像上;反之则显示原图
  3. 通过context创建CGImage的实例
  4. 在主队列中显示到layer上
在此基础上,我们仅仅用加入一些滤镜就能够了。
先在Storyboard上加入一个UIView,再以这个UIView作容器。往里面加四个button,从0到3设置button的tag,并把button们的事件全部连接到VC的applyFilter方法上,UI看起来像这样:

把这个UIView(buttons的容器)连接到VC的filterButtonsContainer上。再加入一个字符串数组,存储一些滤镜的名字。终于VC的全部属性例如以下:

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"]

    }()

......

在viewDidLoad方法中先隐藏滤镜按钮们的容器: 

......

filterButtonsContainer.hidden = true

​......

改动openCamera方法。终于实现例如以下:

@IBAction func openCamera(sender: UIButton) {

    sender.enabled = false

    captureSession.startRunning()

    self.filterButtonsContainer.hidden = false

}

最后applyFilter方法的实现:

@IBAction func applyFilter(sender: UIButton) {

    var filterName = filterNames[sender.tag]

    filter = CIFilter(name: filterName)

}

至此,我们就大功告成了,赶紧在真机上编译、执行看看吧:



保存到图库

接下来我们加入拍照功能。

首先我们在VC上加入一个名为“拍照”的button。连接到VC的takePicture方法上,在实现方法之前。有几步改造工作要先做完。
首先就是图像元数据的问题,一张图像可能包括定位信息、图像格式、方向等元数据。而方向是我们最关心的部分。在上面的viewDidLoad方法中,我是通过将previewLayer进行旋转使我们看到正确的图像,可是假设直接将图像保存在图库或文件里。我们会得到一个方向不对的图像,为了终于获取方向正确的图像,我把previewLayer的旋转去掉:

......

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

......

设置layer的anchorPoint是为了把bounds的顶点从中心变为左上角,这正是UIView的顶点。

如今你执行的话看到的将是方向不对的图像。

然后我们把方向统一放到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之后并在使用滤镜之前调整outputImage的方向。这样一下。四个方向都处理了。

执行之后看到的效果和之前就一样了。

方向处理完后我们还要用一个实例变量保存这个outputImage,由于这里面含有图像的元数据。我们不会丢弃它:

给VC加入一个CIImage的属性: 

var ciImage: CIImage!

在captureSession的回调里保存CIImage:

......

if filter != nil {

    filter.setValue(outputImage, forKey: kCIInputImageKey)

    outputImage = filter.outputImage

}


let cgImage = context.createCGImage(outputImage, fromRect: outputImage.extent())

ciImage = outputImage

......

滤镜处理完后。就将这个CIImage存起来。它可能被应用过滤镜,也可能是干干净净的原图。

最后是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


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
Swift实战-豆瓣电台(二)界面布局发布时间:2022-07-13
下一篇:
iOS10ProgrammingFundamentalswithSwift学习笔记0发布时间:2022-07-13
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap