AVFoundation 相關(guān)類
- AVFoundation 框架基于以下幾個類實現(xiàn)圖像捕捉 ,通過這些類可以訪問來自相機設(shè)備的原始數(shù)據(jù)并控制它的組件。
- AVCaptureDevice
是關(guān)于相機硬件的接口。它被用于控制硬件特性,諸如鏡頭的位置、曝光、閃光燈等。 - AVCaptureDeviceInput
提供來自設(shè)備的數(shù)據(jù)。 - AVCaptureOutput
是一個抽象類,描述 capture session 的結(jié)果。以下是三種關(guān)于靜態(tài)圖片捕捉的具體子類:AVCaptureStillImageOutput用于捕捉靜態(tài)圖片
- AVCaptureMetadataOutput
啟用檢測人臉和二維碼
- AVCaptureVideoOutput
為實時預(yù)覽圖提供原始幀
- AVCaptureSession
管理輸入與輸出之間的數(shù)據(jù)流,以及在出現(xiàn)問題時生成運行時錯誤
。 - AVCaptureVideoPreviewLayer
是 CALayer的子類,可被用于自動顯示相機產(chǎn)生的實時圖像。它還有幾個工具性質(zhì)的方法,可將 layer 上的坐標轉(zhuǎn)化到設(shè)備上。它看起來像輸出,但其實不是。另外,它擁有 session (outputs 被 session 所擁有)。
設(shè)置
讓我們看看如何捕獲圖像。首先我們需要一個 AVCaptureSession
對象:
let session = AVCaptureSession()
現(xiàn)在我們需要一個相機設(shè)備輸入。在大多數(shù) iPhone 和 iPad 中,我們可以選擇后置攝像頭或前置攝像頭 -- 又稱自拍相機 (selfie camera) -- 之一。那么我們必須先遍歷所有能提供視頻數(shù)據(jù)的設(shè)備 (麥克風(fēng)也屬于 AVCaptureDevice
,因此略過不談),并檢查 position
屬性:
let availableCameraDevices=AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo) for device in availableCameraDevices as [AVCaptureDevice] { if device.position == .Back { backCameraDevice = device } else if device.position == .Front { frontCameraDevice = device }}
然后,一旦我們發(fā)現(xiàn)合適的相機設(shè)備,我們就能獲得相關(guān)的 AVCaptureDeviceInput對象。我們會將它設(shè)置為 session 的輸入:
var error:NSError? let possibleCameraInput: AnyObject? = AVCaptureDeviceInput.deviceInputWithDevice(backCameraDevice, error: &error) if let backCameraInput = possibleCameraInput as? AVCaptureDeviceInput { if self.session.canAddInput(backCameraInput) { self.session.addInput(backCameraInput) }}
注意當 app 首次運行時,第一次調(diào)用 AVCaptureDeviceInput.deviceInputWithDevice()
會觸發(fā)系統(tǒng)提示,向用戶請求訪問相機。這在 iOS 7 的時候只有部分國家會有,到了 iOS 8 拓展到了所有地區(qū)。除非得到用戶同意,否則相機的輸入會一直是一個黑色畫面的數(shù)據(jù)流。
對于處理相機的權(quán)限,更合適的方法是先確認當前的授權(quán)狀態(tài)。要是在授權(quán)還沒有確定的情況下 (也就是說用戶還沒有看過彈出的授權(quán)對話框時),我們應(yīng)該明確地發(fā)起請求。
let authorizationStatus = AVCaptureDevice.authorizationStatusForMediaType(AVMediaTypeVideo) switch authorizationStatus { case .NotDetermined: // 許可對話沒有出現(xiàn),發(fā)起授權(quán)許可 AVCaptureDevice.requestAccessForMediaType(AVMediaTypeVideo, completionHandler: { (granted:Bool) -> Void in if granted { // 繼續(xù) } else { // 用戶拒絕,無法繼續(xù) } })case .Authorized: // 繼續(xù)case .Denied, .Restricted: // 用戶明確地拒絕授權(quán),或者相機設(shè)備無法訪問}
如果能繼續(xù)的話,我們會有兩種方式來顯示來自相機的圖像流。最簡單的就是,生成一個帶有 AVCaptureVideoPreviewLayer
的 view,并使用 capture session 作為初始化參數(shù)。
previewLayer = AVCaptureVideoPreviewLayer.layerWithSession(session) as AVCaptureVideoPreviewLayer previewLayer.frame = view.bounds view.layer.addSublayer(previewLayer)
AVCaptureVideoPreviewLayer
會自動地顯示來自相機的輸出。當我們需要將實時預(yù)覽圖上的點擊轉(zhuǎn)換到設(shè)備的坐標系統(tǒng)中,比如點擊某區(qū)域?qū)崿F(xiàn)對焦時,這種做法會很容易辦到。之后我們會看到具體細節(jié)。
第二種方法是從輸出數(shù)據(jù)流捕捉單一的圖像幀,并使用 OpenGL 手動地把它們顯示在 view 上。這有點復(fù)雜,但是如果我們想要對實時預(yù)覽圖進行操作或使用濾鏡的話,就是必要的了。
為獲得數(shù)據(jù)流,我們需要創(chuàng)建一個 AVCaptureVideoDataOutput
,這樣一來,當相機在運行時,我們通過代理方法 captureOutput(_:didOutputSampleBuffer:fromConnection:)
就能獲得所有圖像幀 (除非我們處理太慢而導(dǎo)致掉幀),然后將它們繪制在一個 GLKView中。不需要對 OpenGL 框架有什么深刻的理解,我們只需要這樣就能創(chuàng)建一個 GLKView
glContext = EAGLContext(API: .OpenGLES2) glView = GLKView(frame: viewFrame, context: glContext) ciContext = CIContext(EAGLContext: glContext)
AVCaptureVideoOutput
videoOutput = AVCaptureVideoDataOutput() videoOutput.setSampleBufferDelegate(self, queue: dispatch_queue_create("sample buffer delegate", DISPATCH_QUEUE_SERIAL)) if session.canAddOutput(self.videoOutput) { session.addOutput(self.videoOutput)}
代理方法:
func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!) { let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) let image = CIImage(CVPixelBuffer: pixelBuffer) if glContext != EAGLContext.currentContext() { EAGLContext.setCurrentContext(glContext) } glView.bindDrawable() ciContext.drawImage(image, inRect:image.extent(), fromRect: image.extent()) glView.display()}
一個警告:這些來自相機的樣本旋轉(zhuǎn)了 90 度,這是由于相機傳感器的朝向所導(dǎo)致的。AVCaptureVideoPreviewLayer會自動處理這種情況,但在這個例子,我們需要對 GLKView進行旋轉(zhuǎn)。
- 馬上就要搞定了。最后一個組件 -- AVCaptureStillImageOutput
-- 實際上是最重要的,因為它允許我們捕捉靜態(tài)圖片。只需要創(chuàng)建一個實例,并添加到 session 里去:
stillCameraOutput = AVCaptureStillImageOutput() if self.session.canAddOutput(self.stillCameraOutput) { self.session.addOutput(self.stillCameraOutput)}
配置
現(xiàn)在我們有了所有必需的對象,應(yīng)該為我們的需求尋找最合適的配置。這里又有兩種方法可以實現(xiàn)。最簡單且最推薦是使用
session preset: session.sessionPreset = AVCaptureSessionPresetPhoto
- AVCaptureSessionPresetPhoto
會為照片捕捉選擇最合適的配置,比如它可以允許我們使用最高的感光度 (ISO) 和曝光時間,基于相位檢測 (phase detection)的自動對焦, 以及輸出全分辨率的 JPEG 格式壓縮的靜態(tài)圖片。
然而,如果你需要更多的操控,可以使用 AVCaptureDeviceFormat
這個類,它描述了一些設(shè)備使用的參數(shù),比如靜態(tài)圖片分辨率,視頻預(yù)覽分辨率,自動對焦類型,感光度和曝光時間限制等。每個設(shè)備支持的格式都列在 AVCaptureDevice.formats
屬性中,并可以賦值給 AVCaptureDevice
的 activeFormat
(注意你并不能修改格式)。
操作相機
iPhone 和 iPad 中內(nèi)置的相機或多或少跟其他相機有相同的操作,不同的是,一些參數(shù)如對焦、曝光時間 (在單反相機上的模擬快門的速度),感光度是可以調(diào)節(jié),但是鏡頭光圈是固定不可調(diào)整的。到了 iOS 8,我們已經(jīng)可以對所有這些可變參數(shù)進行手動調(diào)整了。
我們之后會看到細節(jié),不過首先,該啟動相機了:
sessionQueue = dispatch_queue_create("com.example.camera.capture_session", DISPATCH_QUEUE_SERIAL) dispatch_async(sessionQueue) { () -> Void in self.session.startRunning()}
在 session 和相機設(shè)備中完成的所有操作和配置都是利用 block 調(diào)用的。因此,建議將這些操作分配到后臺的串行隊列中。此外,相機設(shè)備在改變某些參數(shù)前必須先鎖定,直到改變結(jié)束才能解鎖,例如:
var error:NSError? if currentDevice.lockForConfiguration(&error) { // 鎖定成功,繼續(xù)配置 // currentDevice.unlockForConfiguration()}else { // 出錯,相機可能已經(jīng)被鎖}
對焦
在 iOS 相機上,對焦是通過移動鏡片改變其到傳感器之間的距離實現(xiàn)的。
自動對焦是通過相位檢測和反差檢測實現(xiàn)的。然而,反差檢測只適用于低分辨率和高 FPS 視頻捕捉 (慢鏡頭)。
編者注 關(guān)于相位對焦和反差對焦(http://ask.zealer.com/post/149)。
- AVCaptureFocusMode
是個枚舉,描述了可用的對焦模式: - Locked
指鏡片處于固定位置 - AutoFocus
指一開始相機會先自動對焦一次,然后便處于 Locked
模式。 - ContinuousAutoFocus
指當場景改變,相機會自動重新對焦到畫面的中心點。
設(shè)置想要的對焦模式必須在鎖定之后實施:
let focusMode:AVCaptureFocusMode = ... if currentCameraDevice.isFocusModeSupported(focusMode) { ... // 鎖定以進行配置 currentCameraDevice.focusMode = focusMode ... // 解鎖 }}
通常情況下,AutoFocus模式會試圖讓屏幕中心成為最清晰的區(qū)域,但是也可以通過變換 “感興趣的點 (point of interest)” 來設(shè)定另一個區(qū)域。這個點是一個 CGPoint,它的值從左上角 {0,0}
到右下角 {1,1},{0.5,0.5}為畫面的中心點。通常可以用視頻預(yù)覽圖上的點擊手勢識別來改變這個點,想要將 view 上的坐標轉(zhuǎn)化到設(shè)備上的規(guī)范坐標,我們可以使用 AVVideoCaptureVideoPreviewLayer.captureDevicePointOfInterestForPoint()
:
var pointInPreview = focusTapGR.locationInView(focusTapGR.view) var pointInCamera = previewLayer.captureDevicePointOfInterestForPoint(pointInPreview) ...// 鎖定,配置// 設(shè)置感興趣的點currentCameraDevice.focusPointOfInterest = pointInCamera// 在設(shè)置的點上切換成自動對焦currentCameraDevice.focusMode = .AutoFocus...// 解鎖
在 iOS 8 中,有個新選項可以移動鏡片的位置,從較近物體的 0.0
到較遠物體的 1.0
(不是指無限遠)。
... // 鎖定,配置var lensPosition:Float = ... // 0.0 到 1.0的float currentCameraDevice.setFocusModeLockedWithLensPosition(lensPosition) { (timestamp:CMTime) -> Void in // timestamp 對應(yīng)于應(yīng)用了鏡片位置的第一張圖像緩存區(qū)}... // 解鎖
這意味著對焦可以使用 UISlider
設(shè)置,這有點類似于旋轉(zhuǎn)單反上的對焦環(huán)。當用這種相機手動對焦時,通常有一個可見的輔助標識指向清晰的區(qū)域。AVFoundation 里面沒有內(nèi)置這種機制,但是比如可以通過顯示 "對焦峰值 (focus peaking)"(一種將已對焦區(qū)域高亮顯示的方式) 這樣的手段來補救。我們在這里不會討論細節(jié),不過對焦峰值可以很容易地實現(xiàn),通過使用閾值邊緣 (threshold edge) 濾鏡 (用自定義 CIFilter
或 GPUImageThresholdEdgeDetectionFilter
),并調(diào)用 AVCaptureAudioDataOutputSampleBufferDelegate
下的 captureOutput(_:didOutputSampleBuffer:fromConnection:)
方法將它覆蓋到實時預(yù)覽圖上。
曝光
在 iOS 設(shè)備上,鏡頭上的光圈是固定的 (在 iPhone 5s 以及其之后的光圈值是 f/2.2,之前的是 f/2.4),因此只有改變曝光時間和傳感器的靈敏度才能對圖片的亮度進行調(diào)整,從而達到合適的效果。至于對焦,我們可以選擇連續(xù)自動曝光,在“感興趣的點”一次性自動曝光,或者手動曝光。除了指定“感興趣的點”,我們可以通過設(shè)置曝光補償 (compensation) 修改自動曝光,也就是曝光檔位的目標偏移。目標偏移在曝光檔數(shù)里有講到,它的范圍在 minExposureTargetBias
與 maxExposureTargetBias
之間,0為默認值 (即沒有“補償”)。
var exposureBias:Float = ... // 在 minExposureTargetBias 和 maxExposureTargetBias 之間的值 ... // 鎖定,配置currentDevice.setExposureTargetBias(exposureBias) { (time:CMTime) -> Void in }... // 解鎖
使用手動曝光,我們可以設(shè)置 ISO 和曝光時間,兩者的值都必須在設(shè)備當前格式所指定的范圍內(nèi)。
var activeFormat = currentDevice.activeFormat var duration:CTime = ... //在activeFormat.minExposureDuration 和 activeFormat.maxExposureDuration 之間的值,或用 AVCaptureExposureDurationCurrent 表示不變 var iso:Float = ... // 在 activeFormat.minISO 和 activeFormat.maxISO 之間的值,或用 AVCaptureISOCurrent 表示不變 ... // 鎖定,配置currentDevice.setExposureModeCustomWithDuration(duration, ISO: iso) { (time:CMTime) -> Void in }... // 解鎖
如何知道照片曝光是否正確呢?我們可以通過 KVO,觀察 AVCaptureDevice
的 exposureTargetOffset
屬性,確認是否在 0 附近。
白平衡
數(shù)碼相機為了適應(yīng)不同類型的光照條件需要補償。這意味著在冷光線的條件下,傳感器應(yīng)該增強紅色部分,而在暖光線下增強藍色部分。在 iPhone 相機中,設(shè)備會自動決定合適的補光,但有時也會被場景的顏色所混淆失效。幸運地是,iOS 8 可以里手動控制白平衡。
自動模式工作方式和對焦、曝光的方式一樣,但是沒有“感興趣的點”,整張圖像都會被納入考慮范圍。在手動模式,我們可以通過開爾文所表示的溫度來調(diào)節(jié)色溫和色彩。典型的色溫值在 2000-3000K (類似蠟燭或燈泡的暖光源) 到 8000K (純凈的藍色天空) 之間。色彩范圍從最小的 -150 (偏綠) 到 150 (偏品紅)。
溫度和色彩可以被用于計算來自相機傳感器的恰當?shù)?RGB 值,因此僅當它們做了基于設(shè)備的校正后才能被設(shè)置。
以下是全部過程:
var incandescentLightCompensation = 3_000 var tint = 0 // 不調(diào)節(jié) let temperatureAndTintValues = AVCaptureWhiteBalanceTemperatureAndTintValues(temperature: incandescentLightCompensation, tint: tint) var deviceGains = currentCameraDevice.deviceWhiteBalanceGainsForTemperatureAndTintValues(temperatureAndTintValues) ... // 鎖定,配置currentCameraDevice.setWhiteBalanceModeLockedWithDeviceWhiteBalanceGains(deviceGains) { (timestamp:CMTime) -> Void in } }... // 解鎖
實時人臉檢測
- AVCaptureMetadataOutput
可以用于檢測人臉和二維碼這兩種物體。很明顯,沒什么人用二維碼 (編者注: 因為在歐美現(xiàn)在二維碼不是很流行,這里是一個惡搞。鏈接的這個 tumblr 博客的主題是 “當人們在掃二維碼時的圖片”,但是 2012 年開博至今沒有任何一張圖片,暗諷二維碼根本沒人在用,這和以中日韓為代表的亞洲用戶群體的使用習(xí)慣完全相悖),因此我們就來看看如何實現(xiàn)人臉檢測。我們只需通過 AVCaptureMetadataOutput
的代理方法捕獲的元對象:
var metadataOutput = AVCaptureMetadataOutput() metadataOutput.setMetadataObjectsDelegate(self, queue: self.sessionQueue) if session.canAddOutput(metadataOutput) { session.addOutput(metadataOutput)}metadataOutput.metadataObjectTypes = [AVMetadataObjectTypeFace]
func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!) { for metadataObject in metadataObjects as [AVMetadataObject] { if metadataObject.type == AVMetadataObjectTypeFace { var transformedMetadataObject = previewLayer.transformedMetadataObjectForMetadataObject(metadataObject) } }
更多關(guān)于人臉檢測與識別的內(nèi)容請查看 (http://objccn.io/issue-21-9)。
捕捉靜態(tài)圖片
最后,我們要做的是捕捉高分辨率的圖像,于是我們調(diào)用 captureStillImageAsynchronouslyFromConnection(connection, completionHandler)
。在數(shù)據(jù)時被讀取時,completion handler 將會在某個未指定的線程上被調(diào)用。
如果設(shè)置使用 JPEG 編碼作為靜態(tài)圖片輸出,不管是通過 session .Photo
預(yù)設(shè)設(shè)定的,還是通過設(shè)備輸出設(shè)置設(shè)定的,sampleBuffer
都會返回包含圖像的元數(shù)據(jù)。如果在 AVCaptureMetadataOutput
中是可用的話,這會包含 EXIF 數(shù)據(jù),或是被識別的人臉等:
dispatch_async(sessionQueue) { () -> Void in let connection = self.stillCameraOutput.connectionWithMediaType(AVMediaTypeVideo) // 將視頻的旋轉(zhuǎn)與設(shè)備同步 connection.videoOrientation = AVCaptureVideoOrientation(rawValue: UIDevice.currentDevice().orientation.rawValue)! self.stillCameraOutput.captureStillImageAsynchronouslyFromConnection(connection) { (imageDataSampleBuffer, error) -> Void in if error == nil { // 如果使用 session .Photo 預(yù)設(shè),或者在設(shè)備輸出設(shè)置中明確進行了設(shè)置 // 我們就能獲得已經(jīng)壓縮為JPEG的數(shù)據(jù) let imageData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(imageDataSampleBuffer) // 樣本緩沖區(qū)也包含元數(shù)據(jù),我們甚至可以按需修改它 let metadata:NSDictionary = CMCopyDictionaryOfAttachments(nil, imageDataSampleBuffer, CMAttachmentMode(kCMAttachmentMode_ShouldPropagate)).takeUnretainedValue() if let image = UIImage(data: imageData) { // 保存圖片,或者做些其他想做的事情 ... } } else { NSLog("error while capturing still image: (error)") } }}
- 當圖片被捕捉的時候,有視覺上的反饋是很好的體驗。想要知道何時開始以及何時結(jié)束的話,可以使用 KVO 來觀察 AVCaptureStillImageOutput
的 isCapturingStillImage
屬性。
分級捕捉
在 iOS 8 還有一個有趣的特性叫“分級捕捉”,可以在不同的曝光設(shè)置下拍攝幾張照片。這在復(fù)雜的光線下拍照顯得非常有用,例如,通過設(shè)定 -1、0、1 三個不同的曝光檔數(shù),然后用 HDR 算法合并成一張。
以下是代碼實現(xiàn):
dispatch_async(sessionQueue) { () -> Void in let connection = self.stillCameraOutput.connectionWithMediaType(AVMediaTypeVideo) connection.videoOrientation = AVCaptureVideoOrientation(rawValue: UIDevice.currentDevice().orientation.rawValue)! var settings = [-1.0, 0.0, 1.0].map { (bias:Float) -> AVCaptureAutoExposureBracketedStillImageSettings in AVCaptureAutoExposureBracketedStillImageSettings.autoExposureSettingsWithExposureTargetBias(bias) } var counter = settings.count self.stillCameraOutput.captureStillImageBracketAsynchronouslyFromConnection(connection, withSettingsArray: settings) { (sampleBuffer, settings, error) -> Void in ... // 保存 sampleBuffer(s) // 當計數(shù)為0,捕捉完成 counter-- }}
原文鏈接 https://www.objc.io/issues/21-camera-and-photos/camera-capture-on-ios/