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 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 |
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()
}
從這個方法開始,就算正式開始了。
?
后面有寫到雖然videoSettings是指定一個字典,但是目前只支持kCVPixelBufferPixelFormatTypeKey,我們用它指定像素的輸出格式,這個參數直接影響到生成圖像的成功與否,由于我打算先做一個實時灰度的效果,所以這里使用kCVPixelFormatType_420YpCbCr8BiPlanarFullRange的輸出格式,關于這個格式的詳細說明,可以看最后面的參數資料3(YUV的維基)。
?
我們現在完成一個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上展示給用戶。?
?
http://stackoverflow.com/questions/6468535/cvpixelbufferlockbaseaddress-why-capture-still-image-using-avfoundation
用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。 |
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
? ? })
}
?
這是一段拓展性、維護性都比較好的代碼了:
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,這個屬性描述了視頻尺寸,雖然這個屬性并不重要,但是我更加懶得把尺寸寫死,它的單位是像素
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<CVPixelBuffer>? =?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!
......
在對圖像應用完濾鏡之后,我們做了這些事情: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)
? ? }
}?
簡單說明下上述代碼的作用:使用濾鏡
上面用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
}?
這上面的代碼基本是復制上一篇里的代碼,改的地方只有兩處:?
?
?
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
轉載于:https://www.cnblogs.com/Free-Thinker/p/5113850.html
總結
以上是生活随笔為你收集整理的iOS8 Core Image In Swift:视频实时滤镜的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 原创:独孤九剑和葵花宝典谁更强?金庸:将
- 下一篇: 想学习Android开发