• 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:视频实时滤镜


    在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

        cancelButtonTitle: "确定")

                    alert.show()

                }

                self.captureSession.startRunning()

                sender.enabled = true

        }

    } 

    先将按钮禁用,session停止执行,再用实例变量ciImage绘制一张CGImage,最后连同元数据一同存进图库中。

    这里须要导入AssetsLibrary库:import AssetsLibrary。writeImageToSavedPhotosAlbum方法的回调
    block用到了跟随闭包语法。

    在真机上编译、执行看看吧。

    注:由于我是用layer来做预览容器的,它没有autoresizingMask这种属性。你会发现横屏的时候就显示不正常了,在iOS 8gh,你能够通过重写VC的下面方法来兼容横屏:

    override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator 

    coordinator: UIViewControllerTransitionCoordinator) {

        previewLayer.bounds.size = size

    }



    录制视频


    前期配置

    这篇文章并不会具体解释AVFoundation框架,但为了完毕Core Image的功能,我们多多少少会说一些。
    我们在VC上加入一个名为“開始录制”的按钮,把按钮本身连接到VC的recordsButton属性上,并把它的事件连接到record方法上。UI看起来像这样:

    为了愉快地进行下去。我先把为VC新增的全部属性列出来:

    ......

    // 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,这个属性描写叙述了视频尺寸,尽管这个属性并不重要,可是我更加懒得把尺寸写死。它的单位是像素
    接下来我们先完毕两个工具方法:movieURLcheckForAndDeleteFile

    func movieURL() -> NSURL {

        var tempDir = NSTemporaryDirectory()

        let urlString = tempDir.stringByAppendingPathComponent("tmpMov.mov")

        return NSURL(fileURLWithPath: urlString)

    }

    这种方法做的事情非常easy,仅仅是构建一个暂时文件夹里的文件URL。

    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)

            }

        }

    }

    这种方法检查了文件是否已存在,假设已存在就删除旧文件,之所以要添加这种方法是由于AVAssetWriter不能在已有的文件URL上写文件,假设文件已存在就会报错。另一点须要注意:我在iOS 7上推断文件是否存在时用的是URL的absoluteString方法,结果导致AVAssetWriter没报错。可是后面的缓冲区出错了,排查了非常久,把absoluteString换成path就好了

    二个工具方法完毕后,我们就開始写最基本的方法。即createWriter方法:

    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("不能加入视频writerinput (assetWriterVideoInput)")

        }

    } 

    这种方法主要是配置项非常多。

    • 首先检查了文件是否存在,假设存在的话就删除旧的暂时文件,不然AVAssetWriter会因无法写入文件而报错
    • 实例化一个AVAssetWriter对象,把须要写的文件URL和文件类型传递给它。再给它一个存储错误信息的指针,方便在出错的时候排查
    • 创建一个outputSettings的字典应用到AVAssetWriterInput对象上。这个对象之前没有提到,但也是相当重要的一个对象。它表示了一个输入设备。比方视频、音频的输入等,不同的设备拥有不同的參数和配置,并不复杂。我们这里就不考虑音频输入了。

      在这个视频的配置里。我们配置了视频的编码。以及用获取到的当前视频设备尺寸(单位像素)初始化了宽、高

    • 设置expectsMediaDataInRealTime为true。这是从摄像头捕获的源中进行实时编码的必要參数
    • 设置了视频的transform,主要也是为了解决方向问题
    • 创建另外一个属性字典去实例化一个AVAssetWriterInputPixelBufferAdaptor对象。我们在视频採集的过程中。会不断地通过这个缓冲区往AVAssetWriter对象里加入内容,实例化的參数中还有AVAssetWriterInput对象,属性字典标识了缓冲区的大小与格式。

    • 最后推断一下是否能加入这个输入设备,尽管大多数情况下推断一定为真。并且为假的情况我们也没办法考虑了。但预先推断还是一个好的编码习惯

    处理每一帧

    上面这些基本性的配置工作完毕后,在正式開始录制视频之前,我们还有最后一步要处理。那就是处理视频的每一帧。事实上在之前我们就已经尝试过处理每一帧了,由于我们做过拍照的实时滤镜功能,如今我们仅仅须要改动AVCaptureSession的回调就可以了。

    由于之前在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(nilself.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!

    ......

    在对图像应用完滤镜之后,我们做了这些事情:
    1. 获取尺寸和时间,这两个值在后面会用到。

      强调一下,时间这个參数是非常重要的,当你有一系列的帧的时候,assetWriter必须知道何时显示他们。我们除了通过CMSampleBufferGetOutputPresentationTimeStamp函数获取之外。也能够手动创建一个时间,比方把每一个缓冲区的时间设置为比上一个缓冲区时间多1/30秒。这就相当于创建一个每秒30帧的视频。可是这不能保证视频时序的真实情况,由于某些滤镜(或者其它操作)可能会耗时过长

    2. 当前是否须要录制视频。录制视频事实上就是写文件的一个过程
    3. 推断assetWriter是否已经准备好输入数据了
    4. 一切都准备好后,我们就先配置一个缓冲区。用CVPixelBufferPoolCreatePixelBuffer函数能创建基于池的缓冲区,它的优点是在创建缓冲区的时候会把之前对assetWriterPixelBufferInput对象的配置项应用到新的缓冲区上。这样就避免了你又一次对新的缓冲区进行配置。有一点须要注意,假设我们的assetWriter还未開始工作,那么当我们调用assetWriterPixelBufferInput的pixelBufferPool时候会得到一个空指针,缓冲区当然也就创建不了了
    5. 我们把缓冲区准备好后,就利用context把图像渲染到里面
    6. 把缓冲区写入到暂时文件里。同一时候得到是否写入成功的返回值
    7. 由于在Swift里CVPixelBufferPoolCreatePixelBuffer函数须要的是一个手动管理引用计数的对象(Unmanaged对象),所以须要自己把它处理一下
    8. 假设第6步失败的话就输出一下
    之前的代码还是保留。由于我们还是须要将每一帧绘制到屏幕上。
    由于这种方法用到了非常多对象,并且比較占用内存。所以我在进入这种方法的时候还手动添加了自己主动释放池:

    autoreleasepool {

        // ....

    } 


    保存视频到图库

    我们之前就加入了recordsButton,并把它连接到了record方法上,如今来实现它:

    @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

        }

    }

    首先是不是在录制,假设是的话就停止录制、保存视频。并清理资源。

    假设还没有開始录制,就创建AVAssetWriter并配置好,然后调用startWriting方法使assetWriter開始工作,不然在回调里取pixelBufferPool的时候取不到,除此之外。还要调用startSessionAtSourceTime方法,调用后者是为了在回调中拿到最新的时间。即currentSampleTime。

    假设不调用这两个方法,在appendPixelBuffer的时候就会有问题。就算最后能保存,也仅仅能得到一个空的视频文件。

    当视频录制的过程開始后,就仅仅有调用finishWriting方法才干停止。我们通过saveMovieToCameraRoll方法把视频写入到图库中,不然这视频也就没机会展示了:

    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。


    标记人脸

    我们先简单的用一个Layer把人脸的区域标记出来。给VC添加一个属性:

    // 标记人脸

    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]

    }

    ......

    这里加入了一个元数据的output对象。加入到captureSession后我们就能在回调中得到图像的元数据,包括检測到的人脸。给metadataObjectTypes属性赋值是为了申明要检測的类型,这句要在添加到captureSession之后调用。

    由于我们要在回调中直接操作Layer的显示,所以我把回调放在主队列中。

    实现AVCaptureMetadataOutput的回调方法:

    // 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( faceBounds.size.width * viewSize.height,

                                            height: faceBounds.size.height * viewSize.width)

            print(faceBounds.origin)

            print("###")

            print(faceLayer!.position)

            print("###")

            print(faceLayer!.bounds)

        }

    } 

    简单说明下上述代码的作用:
    1. 參数中的metadataObjects数组就是AVFoundation框架给我们的关于图像的全部元数据,由于我仅仅设置了须要人脸检測。所以简单推断是否为空后,取出当中的数据就可以。

      在这里我仅仅对第一张脸进行了处理

    2. 接下来初始化Layer,并设置边框
    3. 取到的faceObject对象尽管包括了bounds属性,但并不能直接使用,由于从AVFoundation视频中取到的bounds,是一个0~1之间的数,是相对于图像的百分比,所以我们在设置position时,做了两步:把x、y颠倒。修正方向等问题。我仅仅是简单地适配了Portrait方向,此处能达到目的就可以。再和view的宽、高相乘,事实上是和Layer的父Layer的宽、高相乘。
    4. 设置size也如上
    做的事情比較简单,仅仅是单纯地初始化一个Layer,然后不停地改动它的postion和size就可以了。

    编译、执行后应该能看到例如以下效果:




    使用滤镜

    上面用Layer仅仅是简单的先显示一下人脸的区域,我们没有调整图像输出时的CIImage,所以并不能被录制到视频或被保存图片到图库中。

    接下来我们就改动之前的代码。使其能同一时候支持总体滤镜和部分滤镜。
    首先把VC中记录的属性改一下: 

    ......

    // 标记人脸

    // var faceLayer: CALayer?

    var faceObject: AVMetadataFaceObject?

    ......

    我们就不用Layer作人脸范围的标记了,而是直接把滤镜应用到输出的CIImage上,为此,我们须要在AVCaptureMetadataOutput对象的delegate回调方法中记录识别到的脸部元数据:

    // 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( faceBounds.size.height * viewSize.width,

                                            height: faceBounds.size.width * viewSize.height)

            print(faceBounds.origin)

            print("###")

            print(faceLayer!.position)

            print("###")

            print(faceLayer!.bounds)

            */

        }

    } 

    之前的Layer相关代码都凝视掉。仅仅简单地把识别到的第一张脸记录在VC的属性中。
    然后改动AVCaptureSession的delegate回调,在录制视频的代码之前,全局滤镜的代码之后。加入脸部处理代码:

    ......

    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!)

    }

    ...... 

    我们写了个makeFaceWithImage的方法来专门为脸部应用滤镜。应用的效果是上一篇中提到的马赛克效果。
    makeFaceWithCIImage的方法实现:

    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

    } 

    这上面的代码基本是复制上一篇里的代码,改的地方仅仅有两处:
    1. 把马赛克的效果变大,kCIInputScaleKey默认值为0.5,你能够把这行代码凝视掉后看效果
    2. 计算脸部的中心点和半径,计算方法和之前didOutputMetadataObjects这个delegate回调中的计算方法一样。复制过来就可以了
    假设你看到我的上一篇《iOS8 Core Image In Swift:人脸检測以及马赛克》的话,这里面的实现方式应该就非常清晰了。
    到此。对脸部的滤镜也处理好了,编译、执行,能够得到这种结果:




    GitHub下载地址

    我在GitHub上会保持更新。


    參考资料:

    1. http://weblog.invasivecode.com/post/18445861158/a-very-cool-custom-video-camera-with

    2. https://developer.apple.com/library/mac/documentation/graphicsimaging/conceptual/CoreImaging/ci_intro/ci_intro.html

    3. http://en.wikipedia.org/wiki/YUV


  • 相关阅读:
    Linux下MySQL主从同步配置
    Tortoisegit图文使用教程
    C语言I博客作业06
    第十周助教总结
    C语言I博客作业04
    C语言I博客作业02
    第十一周助教总结
    第十二周助教总结
    第九周助教总结
    C语言I博客作业02
  • 原文地址:https://www.cnblogs.com/zfyouxi/p/5269583.html
Copyright © 2020-2023  润新知