<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屬性設定,可以先看看文檔說明:
後面有寫到雖然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予以顯示
現在在真機上編譯、運作,應該能看到如下的實時灰階效果:
(這張圖是通過手機截屏擷取的,容易手抖,是以不是很清晰)
通過以上幾步可以看到,代碼不是很多,沒有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看起來像這樣:
把這個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)
至此,我們就大功告成了,趕緊在真機上編譯、運作看看吧:
接下來我們添加拍照功能。
首先我們在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!) -> 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,這個屬性描述了視訊尺寸,雖然這個屬性并不重要,但是我更加懶得把尺寸寫死,它的機關是像素
接下來我們先完成兩個工具方法:movieURL和checkForAndDeleteFile。
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()
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成功")
在對圖像應用完濾鏡之後,我們做了這些事情:
擷取尺寸和時間,這兩個值在後面會用到。強調一下,時間這個參數是很重要的,當你有一系列的幀的時候,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()
})
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
println("寫入視訊錯誤:\(errorDescription)")
} else {
self.checkForAndDeleteFile()
println("寫入視訊成功")
self.recordsButton.enabled = true
self.recordsButton.setTitle("開始錄制", forState: UIControlState.Normal)
之前在拍照并儲存的時候,我們使用了尾随閉包文法,這裡使用的是完整文法的閉包。
儲存成功後就可以删除臨時檔案了。
編譯、運作吧:
我們先簡單的用一個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(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回調方法中記錄識别到的臉部中繼資料:
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) -> 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回調中的計算方法一樣,複制過來就行了
到此,對臉部的濾鏡也處理好了,編譯、運作,可以得到這樣的結果:
我在GitHub上會保持更新。
參考資料: