你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> objc.io#21#iOS 上的相機捕捉

objc.io#21#iOS 上的相機捕捉

編輯:IOS開發基礎

第一台 iPhone 問世就裝有相機。在第一個 SKDs 版本中,在 app 裡面整合相機的唯一方法就是使用 UIImagePickerController,但到了 iOS 4,發布了更靈活的 AVFoundation 框架。

在這篇文章裡,我們將會看到如何使用 AVFoundation 捕捉圖像,如何操控相機,以及它在 iOS 8 的新特性。

概述

AVFoundation vs. UIImagePickerController

UIImagePickerController 提供了一種非常簡單的拍照方法。它支持所有的基本功能,比如切換到前置攝像頭,開關閃光燈,點擊屏幕區域實現對焦和曝光,以及在 iOS 8 中像系統照相機應用一樣調整曝光。

然而,當有直接訪問相機的需求時,也可以選擇 AVFoundation 框架。它提供了完全的操作權,例如,以編程方式更改硬件參數,或者操縱實時預覽圖。

AVFoundation 相關類

AVFoundation 框架基於以下幾個類實現圖像捕捉 ,通過這些類可以訪問來自相機設備的原始數據並控制它的組件。

  • AVCaptureDevice 是關於相機硬件的接口。它被用於控制硬件特性,諸如鏡頭的位置、曝光、閃光燈等。

  • AVCaptureDeviceInput 提供來自設備的數據。

  • AVCaptureOutput 是一個抽象類,描述 capture session 的結果。以下是三種關於靜態圖片捕捉的具體子類:

    • AVCaptureStillImageOutput 用於捕捉靜態圖片

    • AVCaptureMetadataOutput 啟用檢測人臉和二維碼

    • AVCaptureVideoOutput 為實時預覽圖提供原始幀

  • AVCaptureSession 管理輸入與輸出之間的數據流,以及在出現問題時生成運行時錯誤。

  • AVCaptureVideoPreviewLayer 是 CALayer 的子類,可被用於自動顯示相機產生的實時圖像。它還有幾個工具性質的方法,可將 layer 上的坐標轉化到設備上。它看起來像輸出,但其實不是。另外,它擁有 session (outputs 被 session 所擁有)。

設置

讓我們看看如何捕獲圖像。首先我們需要一個 AVCaptureSession 對象:

let session = AVCaptureSession()

現在我們需要一個相機設備輸入。在大多數 iPhone 和 iPad 中,我們可以選擇後置攝像頭或前置攝像頭 -- 又稱自拍相機 (selfie camera) -- 之一。那麼我們必須先遍歷所有能提供視頻數據的設備 (麥克風也屬於 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
  }
}

然後,一旦我們發現合適的相機設備,我們就能獲得相關的 AVCaptureDeviceInput 對象。我們會將它設置為 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 首次運行時,第一次調用 AVCaptureDeviceInput.deviceInputWithDevice() 會觸發系統提示,向用戶請求訪問相機。這在 iOS 7 的時候只有部分國家會有,到了 iOS 8 拓展到了所有地區。除非得到用戶同意,否則相機的輸入會一直是一個黑色畫面的數據流。

對於處理相機的權限,更合適的方法是先確認當前的授權狀態。要是在授權還沒有確定的情況下 (也就是說用戶還沒有看過彈出的授權對話框時),我們應該明確地發起請求。

let authorizationStatus = AVCaptureDevice.authorizationStatusForMediaType(AVMediaTypeVideo)  
switch authorizationStatus {  
case .NotDetermined:  
  // 許可對話沒有出現,發起授權許可
  AVCaptureDevice.requestAccessForMediaType(AVMediaTypeVideo,
    completionHandler: { (granted:Bool) -> Void in
    if granted {
      // 繼續
    }
    else {
      // 用戶拒絕,無法繼續
    }
  })
case .Authorized:  
  // 繼續
case .Denied, .Restricted:  
  // 用戶明確地拒絕授權,或者相機設備無法訪問
}

如果能繼續的話,我們會有兩種方式來顯示來自相機的圖像流。最簡單的就是,生成一個帶有 AVCaptureVideoPreviewLayer 的 view,並使用 capture session 作為初始化參數。

previewLayer = AVCaptureVideoPreviewLayer.layerWithSession(session) as AVCaptureVideoPreviewLayer  
previewLayer.frame = view.bounds  
view.layer.addSublayer(previewLayer)

AVCaptureVideoPreviewLayer 會自動地顯示來自相機的輸出。當我們需要將實時預覽圖上的點擊轉換到設備的坐標系統中,比如點擊某區域實現對焦時,這種做法會很容易辦到。之後我們會看到具體細節。

第二種方法是從輸出數據流捕捉單一的圖像幀,並使用 OpenGL 手動地把它們顯示在 view 上。這有點復雜,但是如果我們想要對實時預覽圖進行操作或使用濾鏡的話,就是必要的了。

為獲得數據流,我們需要創建一個 AVCaptureVideoDataOutput,這樣一來,當相機在運行時,我們通過代理方法 captureOutput(_:didOutputSampleBuffer:fromConnection:) 就能獲得所有圖像幀 (除非我們處理太慢而導致掉幀),然後將它們繪制在一個 GLKView 中。不需要對 OpenGL 框架有什麼深刻的理解,我們只需要這樣就能創建一個 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()
}

一個警告:這些來自相機的樣本旋轉了 90 度,這是由於相機傳感器的朝向所導致的。AVCaptureVideoPreviewLayer 會自動處理這種情況,但在這個例子,我們需要對 GLKView 進行旋轉。

馬上就要搞定了。最後一個組件 -- AVCaptureStillImageOutput -- 實際上是最重要的,因為它允許我們捕捉靜態圖片。只需要創建一個實例,並添加到 session 裡去:

stillCameraOutput = AVCaptureStillImageOutput()  
if self.session.canAddOutput(self.stillCameraOutput) {  
  self.session.addOutput(self.stillCameraOutput)
}

配置

現在我們有了所有必需的對象,應該為我們的需求尋找最合適的配置。這裡又有兩種方法可以實現。最簡單且最推薦是使用 session preset:

session.sessionPreset = AVCaptureSessionPresetPhoto

AVCaptureSessionPresetPhoto 會為照片捕捉選擇最合適的配置,比如它可以允許我們使用最高的感光度 (ISO) 和曝光時間,基於相位檢測 (phase detection)的自動對焦, 以及輸出全分辨率的 JPEG 格式壓縮的靜態圖片。

然而,如果你需要更多的操控,可以使用 AVCaptureDeviceFormat 這個類,它描述了一些設備使用的參數,比如靜態圖片分辨率,視頻預覽分辨率,自動對焦類型,感光度和曝光時間限制等。每個設備支持的格式都列在 AVCaptureDevice.formats 屬性中,並可以賦值給 AVCaptureDevice 的 activeFormat (注意你並不能修改格式)。

操作相機

iPhone 和 iPad 中內置的相機或多或少跟其他相機有相同的操作,不同的是,一些參數如對焦、曝光時間 (在單反相機上的模擬快門的速度),感光度是可以調節,但是鏡頭光圈是固定不可調整的。到了 iOS 8,我們已經可以對所有這些可變參數進行手動調整了。

我們之後會看到細節,不過首先,該啟動相機了:

sessionQueue = dispatch_queue_create("com.example.camera.capture_session", DISPATCH_QUEUE_SERIAL)  
dispatch_async(sessionQueue) { () -> Void in  
  self.session.startRunning()
}

在 session 和相機設備中完成的所有操作和配置都是利用 block 調用的。因此,建議將這些操作分配到後台的串行隊列中。此外,相機設備在改變某些參數前必須先鎖定,直到改變結束才能解鎖,例如:

var error:NSError?  
if currentDevice.lockForConfiguration(&error) {  
  // 鎖定成功,繼續配置
  // currentDevice.unlockForConfiguration()
}
else {  
  // 出錯,相機可能已經被鎖
}

對焦

在 iOS 相機上,對焦是通過移動鏡片改變其到傳感器之間的距離實現的。

自動對焦是通過相位檢測和反差檢測實現的。然而,反差檢測只適用於低分辨率和高 FPS 視頻捕捉 (慢鏡頭)。

編者注 關於相位對焦和反差對焦,可以參看這篇文章。

AVCaptureFocusMode 是個枚舉,描述了可用的對焦模式:

  • Locked 指鏡片處於固定位置

  • AutoFocus 指一開始相機會先自動對焦一次,然後便處於 Locked 模式。

  • ContinuousAutoFocus 指當場景改變,相機會自動重新對焦到畫面的中心點。

設置想要的對焦模式必須在鎖定之後實施:

let focusMode:AVCaptureFocusMode = ...  
if currentCameraDevice.isFocusModeSupported(focusMode) {  
  ... // 鎖定以進行配置
  currentCameraDevice.focusMode = focusMode
  ... // 解鎖
  }
}

通常情況下,AutoFocus 模式會試圖讓屏幕中心成為最清晰的區域,但是也可以通過變換 “感興趣的點 (point of interest)” 來設定另一個區域。這個點是一個 CGPoint,它的值從左上角 {0,0} 到右下角 {1,1},{0.5,0.5} 為畫面的中心點。通常可以用視頻預覽圖上的點擊手勢識別來改變這個點,想要將 view 上的坐標轉化到設備上的規范坐標,我們可以使用 AVVideoCaptureVideoPreviewLayer.captureDevicePointOfInterestForPoint():

var pointInPreview = focusTapGR.locationInView(focusTapGR.view)  
var pointInCamera = previewLayer.captureDevicePointOfInterestForPoint(pointInPreview)  
...// 鎖定,配置
// 設置感興趣的點
currentCameraDevice.focusPointOfInterest = pointInCamera
// 在設置的點上切換成自動對焦
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 對應於應用了鏡片位置的第一張圖像緩存區
}
... // 解鎖

這意味著對焦可以使用 UISlider 設置,這有點類似於旋轉單反上的對焦環。當用這種相機手動對焦時,通常有一個可見的輔助標識指向清晰的區域。AVFoundation 裡面沒有內置這種機制,但是比如可以通過顯示 "對焦峰值 (focus peaking)"(一種將已對焦區域高亮顯示的方式) 這樣的手段來補救。我們在這裡不會討論細節,不過對焦峰值可以很容易地實現,通過使用阈值邊緣 (threshold edge) 濾鏡 (用自定義 CIFilter 或 GPUImageThresholdEdgeDetectionFilter),並調用 AVCaptureAudioDataOutputSampleBufferDelegate 下的 captureOutput(_:didOutputSampleBuffer:fromConnection:) 方法將它覆蓋到實時預覽圖上。

曝光

在 iOS 設備上,鏡頭上的光圈是固定的 (在 iPhone 5s 以及其之後的光圈值是 f/2.2,之前的是 f/2.4),因此只有改變曝光時間和傳感器的靈敏度才能對圖片的亮度進行調整,從而達到合適的效果。至於對焦,我們可以選擇連續自動曝光,在“感興趣的點”一次性自動曝光,或者手動曝光。除了指定“感興趣的點”,我們可以通過設置曝光補償 (compensation) 修改自動曝光,也就是曝光檔位的目標偏移。目標偏移在曝光檔數裡有講到,它的范圍在 minExposureTargetBias 與 maxExposureTargetBias 之間,0為默認值 (即沒有“補償”)。

var exposureBias:Float = ... // 在 minExposureTargetBias 和 maxExposureTargetBias 之間的值  
... // 鎖定,配置
currentDevice.setExposureTargetBias(exposureBias) { (time:CMTime) -> Void in  
}
... // 解鎖

使用手動曝光,我們可以設置 ISO 和曝光時間,兩者的值都必須在設備當前格式所指定的范圍內。

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 附近。

白平衡

數碼相機為了適應不同類型的光照條件需要補償。這意味著在冷光線的條件下,傳感器應該增強紅色部分,而在暖光線下增強藍色部分。在 iPhone 相機中,設備會自動決定合適的補光,但有時也會被場景的顏色所混淆失效。幸運地是,iOS 8 可以裡手動控制白平衡。

自動模式工作方式和對焦、曝光的方式一樣,但是沒有“感興趣的點”,整張圖像都會被納入考慮范圍。在手動模式,我們可以通過開爾文所表示的溫度來調節色溫和色彩。典型的色溫值在 2000-3000K (類似蠟燭或燈泡的暖光源) 到 8000K (純淨的藍色天空) 之間。色彩范圍從最小的 -150 (偏綠) 到 150 (偏品紅)。

溫度和色彩可以被用於計算來自相機傳感器的恰當的 RGB 值,因此僅當它們做了基於設備的校正後才能被設置。

以下是全部過程:

var incandescentLightCompensation = 3_000  
var tint = 0 // 不調節  
let temperatureAndTintValues = AVCaptureWhiteBalanceTemperatureAndTintValues(temperature: incandescentLightCompensation, tint: tint)  
var deviceGains = currentCameraDevice.deviceWhiteBalanceGainsForTemperatureAndTintValues(temperatureAndTintValues)  
... // 鎖定,配置
currentCameraDevice.setWhiteBalanceModeLockedWithDeviceWhiteBalanceGains(deviceGains) {  
        (timestamp:CMTime) -> Void in
    }
  }
... // 解鎖

實時人臉檢測

AVCaptureMetadataOutput 可以用於檢測人臉和二維碼這兩種物體。很明顯,沒什麼人用二維碼 (編者注: 因為在歐美現在二維碼不是很流行,這裡是一個惡搞。鏈接的這個 tumblr 博客的主題是 “當人們在掃二維碼時的圖片”,但是 2012 年開博至今沒有任何一張圖片,暗諷二維碼根本沒人在用,這和以中日韓為代表的亞洲用戶群體的使用習慣完全相悖),因此我們就來看看如何實現人臉檢測。我們只需通過 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)
      }
    }

更多關於人臉檢測與識別的內容請查看 Engin 的文章。

捕捉靜態圖片

最後,我們要做的是捕捉高分辨率的圖像,於是我們調用 captureStillImageAsynchronouslyFromConnection(connection, completionHandler)。在數據時被讀取時,completion handler 將會在某個未指定的線程上被調用。

如果設置使用 JPEG 編碼作為靜態圖片輸出,不管是通過 session .Photo 預設設定的,還是通過設備輸出設置設定的,sampleBuffer 都會返回包含圖像的元數據。如果在 AVCaptureMetadataOutput 中是可用的話,這會包含 EXIF 數據,或是被識別的人臉等:

dispatch_async(sessionQueue) { () -> Void in
  let connection = self.stillCameraOutput.connectionWithMediaType(AVMediaTypeVideo)
  // 將視頻的旋轉與設備同步
  connection.videoOrientation = AVCaptureVideoOrientation(rawValue: UIDevice.currentDevice().orientation.rawValue)!
  self.stillCameraOutput.captureStillImageAsynchronouslyFromConnection(connection) {
    (imageDataSampleBuffer, error) -> Void in
    if error == nil {
      // 如果使用 session .Photo 預設,或者在設備輸出設置中明確進行了設置
      // 我們就能獲得已經壓縮為JPEG的數據
      let imageData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(imageDataSampleBuffer)
      // 樣本緩沖區也包含元數據,我們甚至可以按需修改它
      let metadata:NSDictionary = CMCopyDictionaryOfAttachments(nil, imageDataSampleBuffer, CMAttachmentMode(kCMAttachmentMode_ShouldPropagate)).takeUnretainedValue()
      if let image = UIImage(data: imageData) {
        // 保存圖片,或者做些其他想做的事情
        ...
      }
    }
    else {
      NSLog("error while capturing still image: \(error)")
    }
  }
}

當圖片被捕捉的時候,有視覺上的反饋是很好的體驗。想要知道何時開始以及何時結束的話,可以使用 KVO 來觀察 AVCaptureStillImageOutput 的 isCapturingStillImage 屬性。

分級捕捉

在 iOS 8 還有一個有趣的特性叫“分級捕捉”,可以在不同的曝光設置下拍攝幾張照片。這在復雜的光線下拍照顯得非常有用,例如,通過設定 -1、0、1 三個不同的曝光檔數,然後用 HDR 算法合並成一張。

以下是代碼實現:

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)
    // 當計數為0,捕捉完成
    counter--
  }
}

這很像是單個圖像捕捉,但是不同的是 completion handler 被調用的次數和設置的數組的元素個數一樣多。

總結

我們已經詳細看到如何在 iPhone 應用裡面實現拍照的基礎功能(呃…不光是 iPhone,用 iPad 拍照其實也是不錯的)。你也可以查看這個例子。最後說下,iOS 8 允許更精確的捕捉,特別是對於高級用戶,這使得 iPhone 與專業相機之間的差距縮小,至少在手動控制上。不過,不是任何人都喜歡在日常拍照時使用復雜的手動操作界面,因此請合理地使用這些特性。

(原文:Camera Capture on iOS)

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved