iOS8 Core Image In Swift:自動改善圖像以及內置濾鏡的使用
iOS8 Core Image In Swift:更復雜的濾鏡
iOS8 Core Image In Swift:人臉檢測以及馬賽克
iOS8 Core Image In Swift:視頻實時濾鏡
class ViewController: UIViewController , AVCaptureVideoDataOutputSampleBufferDelegate {
var captureSession: AVCaptureSession!
var previewLayer: CALayer!
......
一個previewLayer用來做預覽窗口,還有一個AVCaptureSession則是重點。除此之外,我還對VC實現了AVCaptureVideoDataOutputSampleBufferDelegate協議,這個會在後面說。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運行中執行,總是使用這對方法是一個好的習慣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是不能顯示的,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看起來像這樣: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)
}
至此,我們就大功告成了,趕緊在真機上編譯、運行看看吧:......
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
......
現在你運行的話看到的將是方向不正確的圖像。
然後我們把方向統一放到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
}
......
// 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對象裡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對象,屬性字典標識了緩沖區的大小與格式。最後判斷一下能否添加這個輸入設備,雖然大多數情況下判斷一定為真,而且為假的情況我們也沒辦法考慮了,但預先判斷還是一個好的編碼習慣......
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
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 {
// ....
}
@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)
})
}
之前在拍照並保存的時候,我們使用了尾隨閉包語法,這裡使用的是完整語法的閉包。保存成功後就可以刪除臨時文件了。
編譯、運行吧:
// 標記人臉
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就行了。編譯、運行後應該能看到如下效果:......
// 標記人臉
// 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:人臉檢測以及馬賽克》的話,這裡面的實現方式應該就很清楚了。到此,對臉部的濾鏡也處理好了,編譯、運行,可以得到這樣的結果:參考資料:
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