iOS 上的相機捕捉

是否知否發表於2019-05-04

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

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

#小編這裡推薦一個群:691040931 裡面有大量的書籍和麵試資料,很多的iOS開發者都在裡面交流技術

面試資料截圖.jpg
#概述 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 呼叫的。因此,建議將這些操作分配到後臺的序列佇列中。此外,相機裝置在改變某些引數前必須先鎖定,直到改變結束才能解鎖,例如:

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)
      }
    }
複製程式碼

####捕捉靜態圖片 最後,我們要做的是捕捉高解析度的影像,於是我們呼叫 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 與專業相機之間的差距縮小,至少在手動控制上。不過,不是任何人都喜歡在日常拍照時使用複雜的手動操作介面,因此請合理地使用這些特性。

#小編這裡推薦一個群:691040931 裡面有大量的書籍和麵試資料,很多的iOS開發者都在裡面交流技術

面試資料截圖.jpg

原文地址https://objccn.io/issue-21-3/

相關文章