天天看點

iOS8 Core Image In Swift:視訊實時濾鏡

<a target="_blank" href="http://blog.csdn.net/zhangao0086/article/details/39012231">iOS8 Core Image In Swift:自動改善圖像以及内置濾鏡的使用</a>

<a target="_blank" href="http://blog.csdn.net/zhangao0086/article/details/39120331">iOS8 Core Image In Swift:更複雜的濾鏡</a>

<a target="_blank" href="http://blog.csdn.net/zhangao0086/article/details/39253707">iOS8 Core Image In Swift:人臉檢測以及馬賽克</a>

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

}

這裡先對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: 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屬性設定,可以先看看文檔說明:

iOS8 Core Image In Swift:視訊實時濾鏡

後面有寫到雖然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()

session啟動之後,不出意外的話,回調就開始了,并且是實時回調(這也是為什麼要把delegate回調放在一個GCD隊列中的原因),我們處理

optional func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!)

這個回調就可以了:

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上展示給使用者。

首先這個回調給我們了一個CMSampleBufferRef類型的sampleBuffer,這是Core Media對象,我們可以通過CMSampleBufferGetImageBuffer方法把它轉成Core Video對象。

然後我們把緩沖區的base位址給鎖住了,鎖住base位址是為了使緩沖區的記憶體位址變得可通路,否則在後面就取不到必需的資料,顯示在layer上就隻有黑屏,更詳細的原因可以看這裡:

<a target="_blank" href="http://stackoverflow.com/questions/6468535/cvpixelbufferlockbaseaddress-why-capture-still-image-using-avfoundation">http://stackoverflow.com/questions/6468535/cvpixelbufferlockbaseaddress-why-capture-still-image-using-avfoundation</a>

接下來從緩沖區取圖像的資訊,包括寬、高、每行的位元組數等

因為視訊的緩沖區是YUV格式的,我們要把它的luma部分提取出來

我們為了把緩沖區的圖像渲染到layer上,需要用Core Graphics建立一個顔色空間和圖形上下文,然後通過建立的顔色空間把緩沖區的圖像渲染到上下文中

cgImage就是從緩沖區建立的Core Graphics圖像了(CGImage),最後我們在主線程把它指派給layer的contents予以顯示

現在在真機上編譯、運作,應該能看到如下的實時灰階效果:

iOS8 Core Image In Swift:視訊實時濾鏡

(這張圖是通過手機截屏擷取的,容易手抖,是以不是很清晰)

通過以上幾步可以看到,代碼不是很多,沒有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的回調進行一些修改,變成我們熟悉的方式,就像這樣:

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

        self.previewLayer.contents = cgImage

這是一段拓展性、維護性都比較好的代碼了:

先拿到緩沖區,看從緩沖區直接取到一張CIImage

如果指定了濾鏡,就應用到圖像上;反之則顯示原圖

通過context建立CGImage的執行個體

在主隊列中顯示到layer上

在此基礎上,我們隻用添加一些濾鏡就可以了。

先在Storyboard上添加一個UIView,再以這個UIView作容器,往裡面加四個button,從0到3設定button的tag,并把button們的事件全部連接配接到VC的applyFilter方法上,UI看起來像這樣:

iOS8 Core Image In Swift:視訊實時濾鏡

把這個UIView(buttons的容器)連接配接到VC的filterButtonsContainer上,再添加一個字元串數組,存儲一些濾鏡的名字,最終VC的所有屬性如下:

    @IBOutlet var filterButtonsContainer: UIView!

    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方法,最終實作如下:

    self.filterButtonsContainer.hidden = false

最後applyFilter方法的實作:

@IBAction func applyFilter(sender: UIButton) {

    var filterName = filterNames[sender.tag]

    filter = CIFilter(name: filterName)

至此,我們就大功告成了,趕緊在真機上編譯、運作看看吧:

iOS8 Core Image In Swift:視訊實時濾鏡

接下來我們添加拍照功能。

首先我們在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:

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

ciImage = outputImage

濾鏡處理完後,就将這個CIImage存起來,它可能被應用過濾鏡,也可能是幹幹淨淨的原圖。

最後是takePicture的方法實作:

@IBAction func takePicture(sender: UIButton) {

    captureSession.stopRunning()

    var cgImage = context.createCGImage(ciImage, fromRect: ciImage.extent())

    ALAssetsLibrary().writeImageToSavedPhotosAlbum(cgImage, metadata: ciImage.properties())

        { (url: NSURL!, error :NSError!) -&gt; 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看起來像這樣:

iOS8 Core Image In Swift:視訊實時濾鏡

為了愉快地進行下去,我先把為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,這個屬性描述了視訊尺寸,雖然這個屬性并不重要,但是我更加懶得把尺寸寫死,它的機關是像素

接下來我們先完成兩個工具方法:movieURL和checkForAndDeleteFile。

func movieURL() -&gt; NSURL {

    var tempDir = NSTemporaryDirectory()

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

    return NSURL(fileURLWithPath: urlString)

這個方法做的事情很簡單,隻是建構一個臨時目錄裡的檔案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: &amp;error)

        println("删除之前的臨時檔案")

        if let errorDescription = error?.localizedDescription {

            println(errorDescription)

        }

這個方法檢查了檔案是否已存在,如果已存在就删除舊檔案,之是以要增加這個方法是因為AVAssetWriter不能在已有的檔案URL上寫檔案,如果檔案已存在就會報錯。還有一點需要注意:我在iOS 7上判斷檔案是否存在時用的是URL的absoluteString方法,結果導緻AVAssetWriter沒報錯,但是後面的緩沖區出錯了,排查了很久,把absoluteString換成path就好了。。

二個工具方法完成後,我們就開始寫最主要的方法,即createWriter方法:

func createWriter() {

    self.checkForAndDeleteFile()

    assetWriter = AVAssetWriter(URL: movieURL(), fileType: AVFileTypeQuickTimeMovie, error: &amp;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對象,屬性字典辨別了緩沖區的大小與格式。

最後判斷一下能否添加這個輸入裝置,雖然大多數情況下判斷一定為真,而且為假的情況我們也沒辦法考慮了,但預先判斷還是一個好的編碼習慣

上面這些基本性的配置工作完成後,在正式開始錄制視訊之前,我們還有最後一步要處理,那就是處理視訊的每一幀。其實在之前我們就已經嘗試過處理每一幀了,因為我們做過拍照的實時濾鏡功能,現在我們隻需要修改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&lt;CVPixelBuffer&gt;? = nil

        CVPixelBufferPoolCreatePixelBuffer(nil, self.assetWriterPixelBufferInput?.pixelBufferPool, &amp;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成功")

在對圖像應用完濾鏡之後,我們做了這些事情:

擷取尺寸和時間,這兩個值在後面會用到。強調一下,時間這個參數是很重要的,當你有一系列的幀的時候,assetWriter必須知道何時顯示他們,我們除了通過CMSampleBufferGetOutputPresentationTimeStamp函數擷取之外,也可以手動建立一個時間,比如把每個緩沖區的時間設定為比上一個緩沖區時間多1/30秒,這就相當于建立一個每秒30幀的視訊,但是這不能保證視訊時序的真實情況,因為某些濾鏡(或者其他操作)可能會耗時過長

目前是否需要錄制視訊,錄制視訊其實就是寫檔案的一個過程

判斷assetWriter是否已經準備好輸入資料了

一切都準備好後,我們就先配置一個緩沖區。用CVPixelBufferPoolCreatePixelBuffer函數能建立基于池的緩沖區,它的好處是在建立緩沖區的時候會把之前對assetWriterPixelBufferInput對象的配置項應用到新的緩沖區上,這樣就避免了你重新對新的緩沖區進行配置。有一點需要注意,如果我們的assetWriter還未開始工作,那麼當我們調用assetWriterPixelBufferInput的pixelBufferPool時候會得到一個空指針,緩沖區當然也就建立不了了

我們把緩沖區準備好後,就利用context把圖像渲染到裡面

把緩沖區寫入到臨時檔案中,同時得到是否寫入成功的傳回值

由于在Swift裡CVPixelBufferPoolCreatePixelBuffer函數需要的是一個手動管理引用計數的對象(Unmanaged對象),是以需要自己把它處理一下

如果第6步失敗的話就輸出一下

之前的代碼還是保留,因為我們還是需要将每一幀繪制到螢幕上。

由于這個方法用到了很多對象,而且比較占用記憶體,是以我在進入這個方法的時候還手動增加了自動釋放池:

autoreleasepool {

    // ....

我們之前就加入了recordsButton,并把它連接配接到了record方法上,現在來實作它:

@IBAction func record() {

    if isWriting {

        self.isWriting = false

        assetWriterPixelBufferInput = nil

        recordsButton.enabled = false

        assetWriter?.finishWritingWithCompletionHandler({[unowned self] () -&gt; Void in

            println("錄制完成")

            self.recordsButton.setTitle("進行中...", forState: UIControlState.Normal)

            self.saveMovieToCameraRoll()

        })

        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?) -&gt; Void in

            println("寫入視訊錯誤:\(errorDescription)")

        } else {

            self.checkForAndDeleteFile()

            println("寫入視訊成功")

        self.recordsButton.enabled = true

        self.recordsButton.setTitle("開始錄制", forState: UIControlState.Normal)

之前在拍照并儲存的時候,我們使用了尾随閉包文法,這裡使用的是完整文法的閉包。

儲存成功後就可以删除臨時檔案了。

編譯、運作吧:

iOS8 Core Image In Swift:視訊實時濾鏡

我們先簡單的用一個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 &gt; 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(faceLayer!.bounds)

簡單說明下上述代碼的作用:

參數中的metadataObjects數組就是AVFoundation架構給我們的關于圖像的所有中繼資料,由于我隻設定了需要人臉檢測,是以簡單判斷是否為空後,取出其中的資料即可。在這裡我隻對第一張臉進行了處理

接下來初始化Layer,并設定邊框

取到的faceObject對象雖然包含了bounds屬性,但并不能直接使用,因為從AVFoundation視訊中取到的bounds,是一個0~1之間的數,是相對于圖像的百分比,是以我們在設定position時,做了兩步:把x、y颠倒,修正方向等問題,我隻是簡單地适配了Portrait方向,此處能達到目的即可。再和view的寬、高相乘,其實是和Layer的父Layer的寬、高相乘。

設定size也如上

做的事情比較簡單,隻是單純地初始化一個Layer,然後不停地修改它的postion和size就行了。

編譯、運作後應該能看到如下效果:

iOS8 Core Image In Swift:視訊實時濾鏡

上面用Layer隻是簡單的先顯示一下人臉的區域,我們沒有調整圖像輸出時的CIImage,是以并不能被錄制到視訊或被儲存圖檔到圖庫中。

接下來我們就修改之前的代碼,使其能同時支援整體濾鏡和部分濾鏡。

首先把VC中記錄的屬性改一下: 

// var faceLayer: CALayer?

var faceObject: AVMetadataFaceObject?

我們就不用Layer作人臉範圍的标記了,而是直接把濾鏡應用到輸出的CIImage上,為此,我們需要在AVCaptureMetadataOutput對象的delegate回調方法中記錄識别到的臉部中繼資料:

        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(faceLayer!.bounds)

        */

之前的Layer相關代碼都注釋掉,隻簡單地把識别到的第一張臉記錄在VC的屬性中。

然後修改AVCaptureSession的delegate回調,在錄制視訊的代碼之前,全局濾鏡的代碼之後,添加臉部處理代碼:

if self.filter != nil {    // 之前做的全局濾鏡 

if self.faceObject != nil {    // 臉部處理

    outputImage = self.makeFaceWithCIImage(outputImage, faceObject: self.faceObject!)

...... 

makeFaceWithCIImage的方法實作:

func makeFaceWithCIImage(inputImage: CIImage, faceObject: AVMetadataFaceObject) -&gt; 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

        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回調中的計算方法一樣,複制過來就行了

到此,對臉部的濾鏡也處理好了,編譯、運作,可以得到這樣的結果:

iOS8 Core Image In Swift:視訊實時濾鏡

我在GitHub上會保持更新。

參考資料: