用華為CameraKit實現預覽和拍照
theme: cyanosis highlight: rainbow
前言
前面研究了一下如何在Android手機上獲取超廣角鏡頭:一些獲取您的Android裝置超廣角能力的思路 - 掘金 (juejin.cn)。發現HUAWEI官方有推出過一個相機庫CameraKit,就想著自己接入一下看看效果,順便記錄一些遇到的坑。
流程
使用Gradle整合比較常規,看文件即可:
官方提供的整合流程如下圖:
CameraKit
提供了一個Mode類作為一次拍照流程的相關抽象,可理解為一個Session。
CameraKit
的生命週期:
- 模式建立:CameraKit
提供了多種相機的模式,譬如:普通拍照、人像、夜景等,當然還有錄影相關的。
詳情可參考文件:Mode.Type
- 模式配置:主要是配置預覽解析度、拍照解析度等,還有關於在該模式下的一些操作事件的回撥、資料的回撥。
- 基於模式的操作:比較好理解的是利用Mode
類進行預覽、拍照、縮放等。
- 操作回撥:每當觸發一個操作後,會通過在模式配置下注冊的回撥中回撥相關事件或資料。
- 模式釋放:不需要時釋放資源。
接入
在使用CameraKit
時,一切的前提是需要例項化出CameraKit
物件,它是一個餓漢式的單例,在例項化前會判斷一些約束條件,符合條件後才會建立。
java
CameraKit cameraKit = CameraKit.getInstance(getApplicationContext());
模型建立
在預覽的檢視準備好之後,就可以開始建立模式了,譬如在TextureView#onSurfaceTextureAvailable
後。在建立前還需要新建一個HandlerThread,作為整個相機運作的執行緒。
```java private final ModeStateCallback mModeStateCallback = new ModeStateCallback() { @Override public void onCreated(Mode mode) { super.onCreated(mode); mMode = mode; configMode(); // Mode建立成功,可以開始進行模式配置 }
@Override
public void onCreateFailed(String cameraId, int modeType, int errorCode) {
super.onCreateFailed(cameraId, modeType, errorCode);
}
@Override
public void onConfigured(Mode mode) {
super.onConfigured(mode);
mMode.startPreview(); // Mode配置成功,可以開始預覽
}
@Override
public void onConfigureFailed(Mode mode, int errorCode) {
super.onConfigureFailed(mode, errorCode);
}
@Override
public void onReleased(Mode mode) {
super.onReleased(mode);
}
@Override
public void onFatalError(Mode mode, int errorCode) {
super.onFatalError(mode, errorCode);
}
};
cameraKit.createMode(CameraInfo.FacingType.CAMERA_FACING_BACK,
Mode.Type.NORMAL_MODE, mModeStateCallback, mHandler);
``
-
CameraKit中有**對於手機物理攝像頭進行抽象**,在應用層只會提供前置/後置兩個列舉。這裡使用後置攝像頭
CameraInfo.FacingType.CAMERA_FACING_BACK。
-
Mode.Type.NORMAL_MODE為普通拍照模式,如果有拍攝人像、夜景等其他需求,可對應傳入。
-
ModeStateCallback`用於監聽Mode物件的事件。
- 最後還需要一個屬於HandlerThread的Handler,用於訊息分發。
模式配置
從上述程式碼中ModeStateCallback#onCreated
的回撥可以看到,在成功建立模式後就可以開始配置了。
比較重要的是預覽解析度和拍照解析度,可通過以下程式碼獲取裝置支援的
```java
// 預覽解析度
List
// 拍照解析度
List因為用的是`TextureView`,所以傳入`SurfaceTexture.class`。預覽解析度還需要設定回TextureView中**保證預覽畫面正常**。ps:解析度的篩選邏輯比較常規就不多贅述了,這裡選一個**最大的3:4比例**。
java
textureView.getSurfaceTexture()
.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
```
ps:mMode.getModeCharacteristics()
可以獲取到在該模式下一些支援的引數,除上述的解析度外還有支援的對焦型別、縮放範圍等。可參考:ModeCharacteristics
java
mMode.getModeConfigBuilder()
.addPreviewSurface(new Surface(textureView.getSurfaceTexture()))
.addCaptureImage(pictureSize, ImageFormat.JPEG);
mMode.getModeConfigBuilder().setDataCallback(mActionDataCallback, mHandler);
mMode.getModeConfigBuilder().setStateCallback(mActionStateCallback, mHandler);
mMode.configure();
配置時還需要傳入ActionStateCallback
、ActionDataCallback
物件,用於在該模式下的一些操作事件的回撥、資料的回撥
```java
private final ActionStateCallback mActionStateCallback = new ActionStateCallback() {
@Override
public void onPreview(Mode mode, int state, @Nullable PreviewResult result) {
super.onPreview(mode, state, result);
// 預覽事件回撥
}
@Override
public void onTakePicture(Mode mode, int state, @Nullable TakePictureResult result) {
super.onTakePicture(mode, state, result);
// 拍照事件回撥
}
@Override
public void onFocus(Mode mode, int state, @Nullable FocusResult result) {
super.onFocus(mode, state, result);
// 對焦事件回撥
}
};
private final ActionDataCallback mActionDataCallback = new ActionDataCallback() { @Override public void onImageAvailable(Mode mode, int type, Image image) { super.onImageAvailable(mode, type, image); // 拍照資料回撥 }
@Override
public void onThumbnailAvailable(Mode mode, int type, android.util.Size size, byte[] data) {
super.onThumbnailAvailable(mode, type, size, data);
}
}; ```
開始預覽
ModeStateCallback#onConfigured
回撥後即可呼叫Mode#startPreview
開啟預覽。
java
// ModeStateCallback
@Override
public void onConfigured(Mode mode) {
super.onConfigured(mode);
mMode.startPreview(); // Mode配置成功,可以開始預覽
}
這時您的介面上應該就能看到預覽畫面了。
拍照
Mode#takePicture
觸發拍照
java
mMode.takePicture();
ActionStateCallback#onTakePicture
會回撥拍照相關的事件,包括錯誤事件
java
// ActionStateCallback
@Override
public void onTakePicture(Mode mode, int state, @Nullable TakePictureResult result) {
super.onTakePicture(mode, state, result);
if (state == TakePictureResult.State.CAPTURE_COMPLETED) {
// 拍照完成
} else if (state == TakePictureResult.State.ERROR_CAPTURE_NOT_READY
|| state == TakePictureResult.State.ERROR_FILE_IO
|| state == TakePictureResult.State.ERROR_UNKNOWN
|| state == TakePictureResult.State.ERROR_UNSUPPORTED_OPERATION) {
// 拍照出錯
}
}
可參考:ActionStateCallback.TakePictureResult.State
拍照成功後在ActionDataCallback#onImageAvailable
回撥原圖的Image物件,資料格式為jpg
java
// ActionDataCallback
@Override
public void onImageAvailable(Mode mode, int type, Image image) {
super.onImageAvailable(mode, type, image);
if (type == Type.TAKE_PICTURE) {
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
// 轉成Bitmap?
image.close();
}
}
這樣就會拿到jpg的byte陣列了。
資源釋放
在結束時,需要釋放相關資源,當然還包括建立的HandlerThread,避免記憶體洩漏。
java
if (mMode != null) {
mMode.release();
}
超廣角能力
由於筆者最初是想研究超廣角的,所以也來看一下
java
float[] zooms = mMode.getModeCharacteristics().getSupportedZoom();
mMode.setZoom(zooms[0]);
由於CameraKit
已經幫我們抽象了物理攝像頭,對於後置攝像頭當然也包括那顆超廣角攝像頭。使用以上程式碼可以獲取到當前模式下所支援的縮放範圍,一般是一個長度為2的陣列。在華為P40上zooms[0] = 0.6f
。設定後即可獲得超廣角的預覽。
一些疑難雜症
支援的解析度較少
在華為P40上,通過CameraKit
獲取支援的拍照解析度極少
-
通過
CameraKit
獲取 -
通過
原生Camera2
獲取 -
即使是超廣角鏡頭,通過
原生Camera2
獲取
CameraKit的例項約束條件
這個其實不太算是問題,只是限制罷了。CameraKit
例項化前會判斷
- app是否已經獲取了拍照許可權(ps:個人認為這個這個判斷應該交給呼叫方判斷的。。。)
- 裝置是否支援,支援的範圍如下圖:

筆者有一臺華為MatePad11,是高通晶片的,實測發現並不支援,所以該庫支援的範圍還是比較窄的。
拍照輸出的時間很慢
一般使用Camera2拍照平均在500ms可以輸出,使用CameraKit最快也要2s。如果使用一些更為專業的功能可能會更長,這個沒有細測。 在HUAWEI的社群中也有人提問:CameraKit中拍照速度慢的情況下要5、6秒,太慢了,請問如何優化-華為開發者論壇。
筆者的猜測是,從CameraKit匯入的一些類來看,其依賴的還是Camera2。推測是在輸出到呼叫方之前,CameraKit會呼叫一些系統的服務對影象進行處理,就比如超廣角的輸出是處理過畸變的。還有就是上述說的解析度支援極少,所能選用的3:4解析度已經到4096 * 3072
,導致這些處理比較耗時。
無法獲取預覽幀
接入時發現該庫並沒有很好的提供獲取預覽幀的方法,只能通過在配置Mode時新增多一個Surface
。這裡使用ImageReader
實現,具體可參考Camera2的做法,大同小異。
```java
previewImageReader = ImageReader.newInstance(
previewSize.getWidth(),
previewSize.getHeight(),
ImageFormat.YUV_420_888,
2);
previewImageReader.setOnImageAvailableListener(this, mHandler);
mMode.getModeConfigBuilder()
.addPreviewSurface(new Surface(textureView.getSurfaceTexture()))
.addPreviewSurface(previewImageReader.getSurface())
.addCaptureImage(pictureSize, ImageFormat.JPEG);
mMode.getModeConfigBuilder().setDataCallback(mActionDataCallback, mHandler);
mMode.getModeConfigBuilder().setStateCallback(mActionStateCallback, mHandler);
mMode.configure();
``
這裡需要使用
YUV_420_888`,提高輸出效率。還需要注意的是輸出的Image轉成byte陣列的問題。
還有一點,根據社群的一些反饋,並不是所有裝置都支援這樣同時註冊兩個預覽流,可通過以下方式獲取最大的支援數
java
mMode.getModeCharacteristics().getMaxPreviewSurfaceNumber()
該方法雖然可以穩定獲取到預覽幀,但是隨之而來的是加大了拍照輸出的時間。嚴重的可達到5、6s以上。
另一種思路
這個又有另外一個思路去解決:上述程式碼只註冊一個ImageReader用於獲取YUV_420_888
的預覽幀,再通過GLSurfaceView繪製到檢視上,同時將Image轉成byte陣列作為資料層的回撥。具體的實現這裡不細說了,推薦一個OPPO的Demo,裡面有YUV_420_888
通過OpenGL繪製的邏輯,可以參考一下:oppo/CameraUnit。
這裡需要注意的是:
- ImageReader#OnImageAvailableListener
輸出和GLSurfaceView.Renderer#onDrawFrame
的繪製在兩個執行緒,所以需要保證同步。
- 由於Image轉成byte陣列的過程可能存在耗時,這一塊主要來源於大記憶體的申請和gc,可採用全域性變數避免頻繁的記憶體申請。但由於採用了全域性變數,但又不能因為這個轉換導致繪製的掉幀情況,所以需要一些原子性的變數加以輔助,適當做一些丟幀操作。
以上只是筆者的設想,裡面也有更好的優化空間。
最後
以上就是筆者關於華為CameraKit
的研究記錄。其實都2023年了,在一臺鴻蒙手機上做一些Android開發確實有些不太靠譜。在官方文件中最近一次更新是在2021年,目前HUAWEI還是把焦點放在鴻蒙的更新上,華為社群關於該庫的問題看上去也沒有得到很好的解決。所以以上提到的那些問題可能不會得到很好的解決了。這篇文章也可以當是一篇冷知識看看吧。
- 用華為CameraKit實現預覽和拍照
- 重溫今日頭條螢幕適配方案
- 【基於Flutter&Flame 的飛機大戰開發筆記】展示面板及重新開始選單
- 【基於Flutter&Flame 的飛機大戰開發筆記】利用bloc管理遊戲狀態
- 【基於Flutter&Flame 的飛機大戰開發筆記】子彈升級和補給
- 【基於Flutter&Flame 的飛機大戰開發筆記】重構敵機
- 【基於Flutter&Flame 的飛機大戰開發筆記】子彈發射及碰撞檢測
- 【基於Flutter&Flame 的飛機大戰開發筆記】敵機生成器
- 【基於Flutter&Flame 的飛機大戰開發筆記】搭建專案及建立一架戰機
- 一文搞定移動端接入ncnn模型(包括Android、iOS)
- 在Android上實現Metal的計算Demo
- 移動端執行JS指令碼除錯方案-單元測試
- 為什麼Glide4.x中的AppGlideModule不應該出現在Library中
- CameraX OpenGL預覽的全新版本
-
有關Swift Codable解析成Dictionary
的一些事 - 在iOS應用上進行記憶體監控
- 體驗一下用Metal畫圖
- 在iOS上進行WebP編碼是一種怎樣的體驗之為何cpu佔用如此之高?
- 在iOS上進行WebP編碼是一種怎樣的體驗?