
Android自定義Camera2相機
轉載請標明出處:https://blog.csdn.net/ZhijunHong/article/details/115730693,謝謝~
寫在前面
Google從Android 5.0 L(API 21) 版本,開始引入Camera2(android.hardware.camera2)以取代Camera1(android.hardware.Camera)相機框架,
Camera2相比于之前的Camera1架構完全不同,使用起來比較復雜,與此同時功能也變得非常強大,
此篇博客,能夠幫助你快速構建并理解自定義Camera2相機的關鍵步驟,
完整代碼,請移步:https://github.com/zhijunhong/custom_camera/tree/master/camera2
使用Camera2的優點
通過設計框架的改造和優化,Camera2具備了以下優點:
- 改進了新硬體的性能,使用更先進的API架構;
- 可以獲取更多的幀(預覽/拍照)資訊以及手動控制每一幀的引數;
- 對Camera的控制更加完全(比如支持調整focus distance, 剪裁預覽/拍照圖片);
- 支持更多圖片格式(yuv/raw)以及高速連拍;
- …
自定義Camera2相機
一些概念
1. Pipeline
Camera2的API模型被設計成一個 Pipeline(管道),它按順序處理每一幀的請求并回傳請求結果給客戶端,下面這張來自官方的圖展示了Pipeline的作業流程,我們會通過一個簡單的例子詳細解釋這張圖,

Pipeline示意圖
為了解釋上面的示意圖,假設我們想要同時拍攝兩張不同尺寸的圖片,并且在拍攝的程序中閃光燈必須亮起來,整個拍攝流程如下:
- 創建一個用于從Pipeline獲取圖片的CaptureRequest;
- 修改CaptureRequest的閃光燈配置,讓閃光燈在拍照程序中亮起來;
- 創建兩個不同尺寸的Surface用于接收圖片資料,并且將它們添加到CaptureRequest中;
- 發送配置好的CaptureRequest到Pipeline中等待它回傳拍照結果,
一個新的CaptureRequest會被放入一個被稱作Pending Request Queue的佇列中等待被執行,當In-Flight Capture Queue佇列空閑的時候就會從Pending Request Queue獲取若干個待處理的CaptureRequest,并且根據每一個CaptureRequest 的配置進行Capture操作,最后我們從不同尺寸的Surface中獲取圖片資料并且還會得到一個包含了很多與本次拍照相關的資訊的CaptureResult,流程結束,
2. Supported Hardware Level
相機功能的強大與否和硬體息息相關,不同廠商對 Camera2 的支持程度也不同,所以Camera2定義了一個叫做Supported Hardware Level的重要概念,其作用是將不同設備上的Camera2根據功能的支持情況劃分成多個不同級別以便開發者能夠大概了解當前設備上Camera2的支持情況,截止到Android P為止,從低到高一共有LEGACY、LIMITED、FULL 和 LEVEL_3四個級別:
- LEGACY:向后兼容的級別,處于該級別的設備意味著它只支持Camera1的功能,不具備任何Camera2高級特性;
- LIMITED:除了支持Camera1的基礎功能之外,還支持部分Camera2高級特性的級別;
- FULL:支持所有Camera2的高級特性;
- LEVEL_3:新增更多Camera2高級特性,例如YUV資料的后處理等,
3. Capture
相機的所有操作和引數配置最終都是服務于影像捕獲,例如對焦是為了讓某一個區域的影像更加清晰,調節曝光補償是為了調節影像的亮度等,因此,在Camera2 里面所有的相機操作和引數配置都被抽象成Capture(捕獲),所以不要簡單的把Capture直接理解成是拍照,因為Capture操作可能僅僅是為了讓預覽畫面更清晰而進行對焦而已,如果你熟悉Camera,那你可能會問 setFlashMode() 在哪?setFocusMode() 在哪?takePicture() 在哪?告訴你,它們都是通過Capture 來實作的,
Capture從執行方式上又被細分為【單次模式】、【多次模式】和【重復模式】三種,我們來一一解釋下:
- 單次模式(One-shot):指的是只執行一次的Capture操作,例如設定閃光燈模式、對焦模式和拍一張照片等,多個一次性模式的Capture會進入佇列按順序執行,
- 多次模式(Burst):指的是連續多次執行指定的Capture操作,該模式和多次執行單次模式的最大區別是連續多次Capture期間不允許插入其他任何Capture 操作,例如連續拍攝100張照片,在拍攝這100張照片期間任何新的Capture請求都會排隊等待,直到拍完100張照片,多組多次模式的Capture會進入佇列按順序執行,
- 重復模式(Repeating):指的是不斷重復執行指定的Capture操作,當有其他模式的Capture提交時會暫停該模式,轉而執行其他被模式的Capture,當其他模式的 Capture 執行完畢后又會自動恢復繼續執行該模式的Capture,例如顯示預覽畫面就是不斷 Capture 獲取每一幀畫面,該模式的 Capture 是全域唯一的,也就是新提交的重復模式Capture會覆寫舊的重復模式Capture,
關鍵API
| CameraManager | CameraManager是一個負責查詢和建立相機連接的系統服務,它的功能不多,這里列出幾個CameraManager的關鍵功能: 1.將相機資訊封裝到CameraCharacteristics中,并提獲取CameraCharacteristics實體的方式; 2.根據指定的相機ID連接相機設備; 3.提供將閃光燈設定成手電筒模式的快捷方式, |
|---|---|
| CameraCharacteristics | CameraCharacteristics 是一個只讀的相機資訊提供者,其內部攜帶大量的相機資訊,包括代表相機朝向的 LENS_FACING;判斷閃光燈是否可用的 FLASH_INFO_AVAILABLE;獲取所有可用 AE 模式的 CONTROL_AE_AVAILABLE_MODES 等,如果你對Camera1比較熟悉,那么CameraCharacteristics有點像Camera1的 Camera.CameraInfo 或者 Camera.Parameters, |
| CameraDevice | CameraDevice 代表當前連接的相機設備,它的職責有以下四個: 1.根據指定的引數創建 CameraCaptureSession; 2.根據指定的模板創建 CaptureRequest; 3.關閉相機設備; 4.監聽相機設備的狀態,例如斷開連接、開啟成功和開啟失敗等, 熟悉Camera1的人可能會說CameraDevice就是Camera1的 Camera 類,實則不是,Camera 類幾乎負責了所有相機的操作,而 CameraDevice 的功能則十分的單一,就是只負責建立相機連接的事務,而更加細化的相機操作則交給了稍后會介紹的CameraCaptureSession, |
| Surface | Surface 是一塊用于填充影像資料的記憶體空間,例如你可以使用 SurfaceView 的 Surface 接收每一幀預覽資料用于顯示預覽畫面,也可以使用 ImageReader 的 Surface 接收 JPEG 或 YUV 資料,每一個 Surface 都可以有自己的尺寸和資料格式,你可以從 CameraCharacteristics 獲取某一個資料格式支持的尺寸串列, |
| CameraCaptureSession | CameraCaptureSession 實際上就是配置了目標 Surface 的 Pipeline 實體,我們在使用相機功能之前必須先創建 CameraCaptureSession 實體,一個 CameraDevice 一次只能開啟一個 CameraCaptureSession,絕大部分的相機操作都是通過向 CameraCaptureSession 提交一個 Capture 請求實作的,例如拍照、連拍、設定閃光燈模式、觸摸對焦、顯示預覽畫面等, |
| CaptureRequest | CaptureRequest 是向 CameraCaptureSession 提交 Capture 請求時的資訊載體,其內部包括了本次 Capture 的引數配置和接收影像資料的 Surface,CaptureRequest 可以配置的資訊非常多,包括影像格式、影像解析度、傳感器控制、閃光燈控制、3A 控制等,可以說絕大部分的相機引數都是通過 CaptureRequest 配置的,值得注意的是每一個 CaptureRequest 表示一幀畫面的操作,這意味著你可以精確控制每一幀的 Capture 操作, |
| CaptureResult | CaptureResult 是每一次 Capture 操作的結果,里面包括了很多狀態資訊,包括閃光燈狀態、對焦狀態、時間戳等等,例如你可以在拍照完成的時候,通過 CaptureResult 獲取本次拍照時的對焦狀態和時間戳,需要注意的是,CaptureResult 并不包含任何影像資料,前面我們在介紹 Surface 的時候說了,影像資料都是從 Surface 獲取的, |
| ImageReader | 用于從相機打開的通道中讀取需要的格式的原始影像資料,可以設定多個ImageReader, |
開發流程

1.獲取CameraManager
private val cameraManager: CameraManager by lazy { getSystemService(CameraManager::class.java) }
2.獲取相機資訊
val cameraIdList = cameraManager.cameraIdList
cameraIdList.forEach { cameraId ->
val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId)
if (cameraCharacteristics.isHardwareLevelSupported(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL)) {
if (cameraCharacteristics[CameraCharacteristics.LENS_FACING] == CameraCharacteristics.LENS_FACING_FRONT) {
frontCameraId = cameraId
frontCameraCharacteristics = cameraCharacteristics
} else if (cameraCharacteristics[CameraCharacteristics.LENS_FACING] == CameraCharacteristics.LENS_FACING_BACK) {
backCameraId = cameraId
backCameraCharacteristics = cameraCharacteristics
}
}
}
通過CameraManager獲取到所有攝像頭cameraId,通過回圈判斷是前攝像頭(CameraCharacteristics.LENS_FACING_FRONT)還是后攝像頭(CameraCharacteristics.LENS_FACING_BACK)
3.初始化ImageReader
private var jpegImageReader: ImageReader? = null
jpegImageReader = ImageReader.newInstance(imageSize.width, imageSize.height, ImageFormat.JPEG, 5)
jpegImageReader?.setOnImageAvailableListener(OnJpegImageAvailableListener(), cameraHandler)
......
private inner class OnJpegImageAvailableListener : ImageReader.OnImageAvailableListener {
private val dateFormat: DateFormat = SimpleDateFormat("yyyyMMddHHmmssSSS", Locale.getDefault())
private val cameraDir: String = "${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)}/Camera"
@WorkerThread
override fun onImageAvailable(imageReader: ImageReader) {
val image = imageReader.acquireNextImage()
val captureResult = captureResults.take()
if (image != null && captureResult != null) {
image.use {
val jpegByteBuffer = it.planes[0].buffer// Jpeg image data only occupy the planes[0].
val jpegByteArray = ByteArray(jpegByteBuffer.remaining())
jpegByteBuffer.get(jpegByteArray)
val width = it.width
val height = it.height
saveImageExecutor.execute {
val date = System.currentTimeMillis()
val title = "IMG_${dateFormat.format(date)}"// e.g. IMG_20190211100833786
val displayName = "$title.jpeg"// e.g. IMG_20190211100833786.jpeg
val path = "$cameraDir/$displayName"// e.g. /sdcard/DCIM/Camera/IMG_20190211100833786.jpeg
val orientation = captureResult[CaptureResult.JPEG_ORIENTATION]
val location = captureResult[CaptureResult.JPEG_GPS_LOCATION]
val longitude = location?.longitude ?: 0.0
val latitude = location?.latitude ?: 0.0
// Write the jpeg data into the specified file.
File(path).writeBytes(jpegByteArray)
// Insert the image information into the media store.
val values = ContentValues()
values.put(MediaStore.Images.ImageColumns.TITLE, title)
values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, displayName)
values.put(MediaStore.Images.ImageColumns.DATA, path)
values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, date)
values.put(MediaStore.Images.ImageColumns.WIDTH, width)
values.put(MediaStore.Images.ImageColumns.HEIGHT, height)
values.put(MediaStore.Images.ImageColumns.ORIENTATION, orientation)
values.put(MediaStore.Images.ImageColumns.LONGITUDE, longitude)
values.put(MediaStore.Images.ImageColumns.LATITUDE, latitude)
contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
// Refresh the thumbnail of image.
val thumbnail = getThumbnail(path)
if (thumbnail != null) {
runOnUiThread {
thumbnailView.setImageBitmap(thumbnail)
thumbnailView.scaleX = 0.8F
thumbnailView.scaleY = 0.8F
thumbnailView.animate().setDuration(50).scaleX(1.0F).scaleY(1.0F).start()
}
}
}
}
}
}
}
ImageReader是獲取影像資料的重要途徑,通過它可以獲取到不同格式的影像資料,例如JPEG、YUV、RAW等,通過ImageReader.newInstance(int width, int height, int format, int maxImages)創建ImageReader物件,有4個引數:
- width:影像資料的寬度
- height:影像資料的高度
- format:影像資料的格式,例如
ImageFormat.JPEG,ImageFormat.YUV_420_888等 - maxImages:最大Image個數,Image物件池的大小,指定了能從ImageReader獲取Image物件的最大值,過多獲取緩沖區可能導致OOM,所以最好按照最少的需要去設定這個值
ImageReader其他相關的方法和回呼:
ImageReader.OnImageAvailableListener:有新影像資料的回呼acquireLatestImage():從ImageReader的佇列里面,獲取最新的Image,洗掉舊的,如果沒有可用的Image,回傳nullacquireNextImage():獲取下一個最新的可用Image,沒有則回傳nullclose():釋放與此ImageReader關聯的所有資源getSurface():獲取為當前ImageReader生成Image的Surface
4.打開相機設備
val cameraStateCallback = CameraStateCallback()
cameraManager.openCamera(cameraId, cameraStateCallback, mainHandler)
......
private inner class CameraStateCallback : CameraDevice.StateCallback() {
@MainThread
override fun onOpened(camera: CameraDevice) {
cameraDeviceFuture!!.set(camera)
cameraCharacteristicsFuture!!.set(getCameraCharacteristics(camera.id))
}
@MainThread
override fun onClosed(camera: CameraDevice) {
}
@MainThread
override fun onDisconnected(camera: CameraDevice) {
cameraDeviceFuture!!.set(camera)
closeCamera()
}
@MainThread
override fun onError(camera: CameraDevice, error: Int) {
cameraDeviceFuture!!.set(camera)
closeCamera()
}
}
cameraManager.openCamera(@NonNull String cameraId,@NonNull final CameraDevice.StateCallback callback, @Nullable Handler handler)的三個引數:
- cameraId:攝像頭的唯一標識
- callback:設備連接狀態變化的回呼
- handler:回呼執行的Handler物件,傳入null則使用當前的主執行緒Handler
其中CameraStateCallback回呼:
- onOpened:表示相機打開成功,可以真正開始使用相機,創建Capture會話
- onDisconnected:當相機斷開連接時回呼該方法,需要進行釋放相機的操作
- onError:當相機打開失敗時,需要進行釋放相機的操作
- onClosed:呼叫Camera.close()后的回呼方法
5.創建Capture Session
val sessionStateCallback = SessionStateCallback()
......
val cameraDevice = cameraDeviceFuture?.get()
cameraDevice?.createCaptureSession(outputs, sessionStateCallback, mainHandler)
......
private inner class SessionStateCallback : CameraCaptureSession.StateCallback() {
@MainThread
override fun onConfigureFailed(session: CameraCaptureSession) {
captureSessionFuture!!.set(session)
}
@MainThread
override fun onConfigured(session: CameraCaptureSession) {
captureSessionFuture!!.set(session)
}
@MainThread
override fun onClosed(session: CameraCaptureSession) {
}
}
這段的代碼核心方法是mCameraDevice.createCaptureSession()創建Capture會話,它接受了三個引數:
- outputs:用于接受影像資料的surface集合,這里傳入的是一個preview的surface
- callback:用于監聽 Session 狀態的CameraCaptureSession.StateCallback物件
- handler:用于執行CameraCaptureSession.StateCallback的Handler物件,傳入null則使用當前的主執行緒Handler
6.創建CaptureRequest
CaptureRequest是向CameraCaptureSession提交Capture請求時的資訊載體,其內部包括了本次Capture的引數配置和接收影像資料的Surface
if (cameraDevice != null) {
previewImageRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
captureImageRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
}
......
val cameraDevice = cameraDeviceFuture?.get()
val captureSession = captureSessionFuture?.get()
val previewImageRequestBuilder = previewImageRequestBuilder!!
val captureImageRequestBuilder = captureImageRequestBuilder!!
if (cameraDevice != null && captureSession != null) {
val previewSurface = previewSurface!!
val previewDataSurface = previewDataSurface
previewImageRequestBuilder.addTarget(previewSurface)
// Avoid missing preview frame while capturing image.
captureImageRequestBuilder.addTarget(previewSurface)
if (previewDataSurface != null) {
previewImageRequestBuilder.addTarget(previewDataSurface)
// Avoid missing preview data while capturing image.
captureImageRequestBuilder.addTarget(previewDataSurface)
}
val previewRequest = previewImageRequestBuilder.build()
captureSession.setRepeatingRequest(previewRequest, RepeatingCaptureStateCallback(), mainHandler)
}
......
private inner class RepeatingCaptureStateCallback : CameraCaptureSession.CaptureCallback() {
@MainThread
override fun onCaptureStarted(session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long) {
super.onCaptureStarted(session, request, timestamp, frameNumber)
}
@MainThread
override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
super.onCaptureCompleted(session, request, result)
}
}
除了模式的配置,CaptureRequest還可以配置很多其他資訊,例如影像格式、影像解析度、傳感器控制、閃光燈控制、3A(自動對焦-AF、自動曝光-AE和自動白平衡-AWB)控制等,在createCaptureSession的回呼中可以進行設定,最后通過build()方法生成CaptureRequest物件,
7.預覽
Camera2中,通過連續重復的Capture實作預覽功能,每次Capture會把預覽畫面顯示到對應的Surface上,連續重復的Capture操作通過
captureSession.setRepeatingRequest(previewRequest, RepeatingCaptureStateCallback(), mainHandler)
實作,該方法有三個引數:
- request:CaptureRequest物件
- listener:監聽Capture 狀態的回呼
- handler:用于執行CameraCaptureSession.CaptureCallback的Handler物件,傳入null則使用當前的主執行緒Handler
停止預覽使用mCaptureSession.stopRepeating()方法,
8.拍照
設定上面的request,session后,就可以真正的開始拍照操作
val captureImageRequest = captureImageRequestBuilder.build()
captureSession.capture(captureImageRequest, CaptureImageStateCallback(), mainHandler)
......
private inner class CaptureImageStateCallback : CameraCaptureSession.CaptureCallback() {
@MainThread
override fun onCaptureStarted(session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long) {
super.onCaptureStarted(session, request, timestamp, frameNumber)
// Play the shutter click sound.
cameraHandler?.post { mediaActionSound.play(MediaActionSound.SHUTTER_CLICK) }
}
@MainThread
override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
super.onCaptureCompleted(session, request, result)
captureResults.put(result)
}
}
captureSession.capture()方法也有三個引數,和mCaptureSession.setRepeatingRequest一樣:
- request:CaptureRequest物件
- listener:監聽Capture 狀態的回呼
- handler:用于執行CameraCaptureSession.CaptureCallback的Handler物件,傳入null則使用當前的主執行緒Handler
9.關閉相機
和其他硬體資源的使用一樣,當我們不再需要使用相機時記得呼叫 CameraDevice.close() 方法及時關閉相機回收資源,關閉相機的操作至關重要,因為如果你一直占用相機資源,其他基于相機開發的功能都會無法正常使用,嚴重情況下直接導致其他相機相關的 APP 無法正常使用,當相機被完全關閉的時候會通過 CameraStateCallback.onCllosed() 方法通知你相機已經被關閉,那么在什么時候關閉相機最合適呢?個人的建議是在 onPause() 的時候就一定要關閉相機,因為在這個時候相機頁面已經不是用戶關注的焦點,大部分情況下已經可以關閉相機了,
cameraDevice?.close()
previewDataImageReader?.close()
jpegImageReader?.close()
先后對CaptureSession,CameraDevice,ImageReader進行close操作,釋放資源,
從 Camera1遷移到Camera2的建議
如果你的專案正在使用Camera1,并且打算從Camera1遷移到Camera2的話,希望以下幾個建議可以對你有所幫助:
- Camera1嚴格區分了預覽和拍照兩個流程,而Camera2則把這兩個流程都抽象成了Capture行為,所以建議你不要帶著過多的Camera1思維使用Camera2,避免因為思維上的束縛而無法充分利用Camera2靈活的 API;
- 如同Camera1一樣,Camera2的一些 API呼叫也會耗時,所以建議你使用獨立的執行緒執行所有的相機操作,盡量避免直接在主執行緒呼叫Camera2的API,HandlerThread 是一個不錯的選擇;
- 可以認為Camera1是Camera2的一個子集,也就是說Camera1能做的事情Camera2一定能做,反過來則不一定行得通;
- 如果你的應用程式需要同時兼容Camera1 和Camera2,個人建議分開維護,因為Camera1蹩腳的API設計很可能讓Camera2靈活的API無法得到充分的發揮,另外將兩個設計上完全不兼容的東西攪和在一起帶來的痛苦可能遠大于其帶來便利性,多寫一些冗余的代碼也許還更開心;
- 官方說Camera2的性能會更好,但在較早期的一些機器上運行Camera2的性能并沒有比Camera1好多少;
- 當設備的 Supported Hardware Level 低于FULL的時候,建議還是使用Camera1,因為FULL級別以下的 Camera2 能提供的功能幾乎和Camera1一樣,所以倒不如選擇更加穩定的Camera1,
完整代碼:https://github.com/zhijunhong/custom_camera/tree/master/camera2
最后,別忘了start喲~
參考:
Android Camera-Camera2使用
Android Camera2 教程
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/277054.html
標籤:其他
上一篇:android NDK開發決議
