你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> iOS8 Core Image In Swift:視頻實時濾鏡

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

編輯:IOS開發綜合

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(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屬性設置,可以先看看文檔說明:
\
後面有寫到雖然videoSettings是指定一個字典,但是目前只支持kCVPixelBufferPixelFormatTypeKey,我們用它指定像素的輸出格式,這個參數直接影響到生成圖像的成功與否,由於我打算先做一個實時灰度的效果,所以這裡使用kCVPixelFZ喎?/kf/ware/vc/" target="_blank" class="keylink">vcm1hdFR5cGVfNDIwWXBDYkNyOEJpUGxhbmFyRnVsbFJhbmdlPC9zdHJvbmc+tcTK5LP2uPHKvaOsudjT2tXiuPa48cq9tcTP6s+4y7XD96Osv8nS1L+01+6688PmtcSyzsr918rBzzOjqFlVVrXEzqy7+aOpoaO688PmyejWw8HLPHN0cm9uZz5hbHdheXNEaXNjYXJkc0xhdGVWaWRlb0ZyYW1lczwvc3Ryb25nPrLOyv2jrLHtyr62qsb60dOz2bXE1qGju82s0fnTw2NhbkFkZElucHV0t723qMXQts+yosztvNO1vXNlc3Npb27W0KGj1+6688no1sNkZWxlZ2F0ZbvYtfejqEFWQ2FwdHVyZVZpZGVvRGF0YU91dHB1dFNhbXBsZUJ1ZmZlckRlbGVnYXRl0K3S6aOpus272LX3yrHL+bSmtcRHQ0S208HQo6yyoszhvbvQ3rjEtcTF5NbDoaM8cD48L3A+PHA+ztLDx8/W1NrN6rPJ0ru49nNlc3Npb261xL2owaK5/bPMo6y1q9XiuPZzZXNzaW9uu7nDu9PQv6rKvLmk1/ejrL7Nz/HO0sPHt8POysr9vt2/4rXEyrG68qOs0qrPyLTyv6rK/b7dv+ItLS3Iu7rzvajBosGsvdMtLS23w87Kyv2+3S0tLbnYsdXBrL3TLS0tudix1cr9vt2/4tK70fmjrM7Sw8fU2jxzdHJvbmc+b3BlbkNhbWVyYTwvc3Ryb25nPre9t6jA78b0tq9zZXNzaW9uo7ogPC9wPjxwPjwvcD48cD5ASUJBY3Rpb24gZnVuYyBvcGVuQ2FtZXJhKHNlbmRlcjogVUlCdXR0b24pIHs8L3A+PHA+ICAgIHNlbmRlci5lbmFibGVkID0gZmFsc2U8L3A+PHA+ICAgIGNhcHR1cmVTZXNzaW9uLnN0YXJ0UnVubmluZygpPC9wPjxwPn08L3A+c2Vzc2lvbsb0tq/Wrrrzo6yyu7P20uLN4rXEu7CjrLvYtfe+zb+qyrzBy6OssqLH0srHyrXKsbvYtfejqNXi0rLKx86qyrLDtNKqsNFkZWxlZ2F0ZbvYtfe3xdTa0ru49kdDRLbTwdDW0LXE1K3S8qOpo6zO0sPHtKbA7TxwPjwvcD48cD48c3Ryb25nPm9wdGlvbmFsIGZ1bmMgY2FwdHVyZU91dHB1dChjYXB0dXJlT3V0cHV0OiBBVkNhcHR1cmVPdXRwdXQhLCBkaWRPdXRwdXRTYW1wbGVCdWZmZXIgc2FtcGxlQnVmZmVyOiBDTVNhbXBsZUJ1ZmZlciEsIGZyb21Db25uZWN0aW9uIGNvbm5lY3Rpb246IEFWQ2FwdHVyZUNvbm5lY3Rpb24hKTwvc3Ryb25nPjwvcD48cD7V4rj2u9i1977Nv8nS1MHLo7o8L3A+PHA+PGJyIC8+PC9wPjxoMz5Db3JlIEltYWdl1q7HsLXEt73KvTwvaDM+PHA+ZnVuYyBjYXB0dXJlT3V0cHV0KGNhcHR1cmVPdXRwdXQ6IEFWQ2FwdHVyZU91dHB1dCEsPC9wPjxwPiAgICAgICAgICAgICAgICAgICAgZGlkT3V0cHV0U2FtcGxlQnVmZmVyIHNhbXBsZUJ1ZmZlcjogQ01TYW1wbGVCdWZmZXIhLDwvcD48cD4gICAgICAgICAgICAgICAgICAgIGZyb21Db25uZWN0aW9uIGNvbm5lY3Rpb246IEFWQ2FwdHVyZUNvbm5lY3Rpb24hKSB7PC9wPjxwPjxiciAvPjwvcD48cD4gICAgbGV0IGltYWdlQnVmZmVyID0gQ01TYW1wbGVCdWZmZXJHZXRJbWFnZUJ1ZmZlcihzYW1wbGVCdWZmZXIpPC9wPjxwPjxiciAvPjwvcD48cD4gICAgQ1ZQaXhlbEJ1ZmZlckxvY2tCYXNlQWRkcmVzcyhpbWFnZUJ1ZmZlciwgMCk8L3A+PHA+PGJyIC8+PC9wPjxwPiAgICBsZXQgd2lkdGggPSBDVlBpeGVsQnVmZmVyR2V0V2lkdGhPZlBsYW5lKGltYWdlQnVmZmVyLCAwKTwvcD48cD4gICAgbGV0IGhlaWdodCA9IENWUGl4ZWxCdWZmZXJHZXRIZWlnaHRPZlBsYW5lKGltYWdlQnVmZmVyLCAwKTwvcD48cD4gICAgbGV0IGJ5dGVzUGVyUm93ID0gQ1ZQaXhlbEJ1ZmZlckdldEJ5dGVzUGVyUm93T2ZQbGFuZShpbWFnZUJ1ZmZlciwgMCk8L3A+PHA+ICAgIGxldCBsdW1hQnVmZmVyID0gQ1ZQaXhlbEJ1ZmZlckdldEJhc2VBZGRyZXNzT2ZQbGFuZShpbWFnZUJ1ZmZlciwgMCk8L3A+PHA+ICAgIDwvcD48cD4gICAgbGV0IGdyYXlDb2xvclNwYWNlID0gQ0dDb2xvclNwYWNlQ3JlYXRlRGV2aWNlR3JheSgpPC9wPjxwPiAgICBsZXQgY29udGV4dCA9IENHQml0bWFwQ29udGV4dENyZWF0ZShsdW1hQnVmZmVyLCB3aWR0aCwgaGVpZ2h0LCA4LCBieXRlc1BlclJvdywgZ3JheUNvbG9yU3BhY2UsIENHQml0bWFwSW5mby5hbGxaZXJvcyk8L3A+PHA+ICAgIGxldCBjZ0ltYWdlID0gQ0dCaXRtYXBDb250ZXh0Q3JlYXRlSW1hZ2UoY29udGV4dCk8L3A+PHA+ICAgIDwvcD48cD4gICAgZGlzcGF0Y2hfc3luYyhkaXNwYXRjaF9nZXRfbWFpbl9xdWV1ZSgpLCB7PC9wPjxwPiAgICAgICAgc2VsZi5wcmV2aWV3TGF5ZXIuY29udGVudHMgPSBjZ0ltYWdlPC9wPjxwPiAgICB9KTwvcD48cD59PC9wPrWxyv2+3bu6s+XH+LXExNrI3bj80MK1xMqxuvKjrEFWRm91bmRhdGlvbr7Nu+HC7cnPtffV4rj2u9i196Osy/nS1M7Sw8e/ydLU1NrV4sDvytW8r8rTxrW1xMO/0rvWoaOsvq25/bSmwO3Wrrrz1Nnk1si+tb1sYXllcsnP1bnKvrj408O7p6GjPHA+PC9wPjxwPjwvcD7K18/I1eK49rvYtfe4+M7Sw8fBy9K7uPY8c3Ryb25nPkNNU2FtcGxlQnVmZmVyUmVmPC9zdHJvbmc+wODQzbXEc2FtcGxlQnVmZmVyo6zV4srHQ29yZSBNZWRpYbbUz/OjrM7Sw8e/ydLUzai5/UNNU2FtcGxlQnVmZmVyR2V0SW1hZ2VCdWZmZXK3vbeosNHL/Neqs8lDb3JlIFZpZGVvttTP86GjyLu6887Sw8ew0bu6s+XH+LXEYmFzZbXY1re4+Mv416HBy6Osy/jXoWJhc2W12Na3ysfOqsHLyrm7urPlx/i1xMTatOa12Na3seS1w7/Jt8POyqOst/HU8tTauvPD5r7NyKGyu7W9sdjQ6LXEyv2+3aOsz9TKvtTabGF5ZXLJz77N1rvT0LraxsGjrLj8z+rPuLXE1K3S8r/J0tS/tNXiwO+jujxiciAvPmh0dHA6Ly9zdGFja292ZXJmbG93LmNvbS9xdWVzdGlvbnMvNjQ2ODUzNS9jdnBpeGVsYnVmZmVybG9ja2Jhc2VhZGRyZXNzLXdoeS1jYXB0dXJlLXN0aWxsLWltYWdlLXVzaW5nLWF2Zm91bmRhdGlvbr3Tz8LAtLTTu7qz5cf4yKHNvM/xtcTQxc+io6yw/MCov+2horjfoaLDv9DQtcTX1r3ayv21yNLyzqrK08a1tcS7urPlx/jKx1lVVrjxyr21xKOsztLDx9KqsNHL/LXEbHVtYbK/t9bM4cihs/bAtM7Sw8fOqsHLsNG7urPlx/i1xM28z/Hk1si+tb1sYXllcsnPo6zQ6NKq08NDb3JlIEdyYXBoaWNztLS9qNK7uPbR1cmrv9W85LrNzbzQzsnPz8LOxKOsyLu6882ouf20tL2otcTR1cmrv9W85LDRu7qz5cf4tcTNvM/x5NbIvrW9yc/Pws7E1tBjZ0ltYWdlvs3Kx7TTu7qz5cf4tLS9qLXEQ29yZSBHcmFwaGljc828z/HBy6OoQ0dJbWFnZaOpo6zX7rrzztLDx9Ta1vfP37PMsNHL/Liz1rW4+GxheWVytcRjb250ZW50c9Po0tTP1Mq+z9bU2tTa1ea7+snPseDS66Gi1MvQ0KOs06a4w8Tcv7S1vcjnz8K1xMq1yrG70rbI0Ke5+6O6PGltZyBzcmM9"/uploadfile/Collfiles/20140929/2014092909160798.png" alt="\" />
(這張圖是通過手機截屏獲取的,容易手抖,所以不是很清晰)

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

// 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上在此基礎上,我們只用添加一些濾鏡就可以了。先在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)

}

這個方法做的事情很簡單,只是構建一個臨時目錄裡的文件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("不能添加視頻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? = 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 {

// ....

}


保存視頻到圖庫

我們之前就加入了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(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也如上做的事情比較簡單,只是單純地初始化一個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(width: 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

}

這上面的代碼基本是復制上一篇裡的代碼,改的地方只有兩處:把馬賽克的效果變大,kCIInputScaleKey默認值為0.5,你可以把這行代碼注釋掉後看效果計算臉部的中心點和半徑,計算方法和之前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


  1. 上一頁:
  2. 下一頁:
Copyright © Ios教程網 All Rights Reserved