Android Camera 開發簡單例項(一): Preview
技術標籤:Android Camerajavaandroid
目前正在學習camera開發,初步學習camera1,即android.hardware.camera。
一.Function描述
preview功能是camera功能的基礎,相機進入拍照狀態必需要在preview之後,且使用者的體驗很大程度上與preview的幀率以及解析度有關。
關於相機工作流程,以拍攝一次舉例:
1.判斷surface 是否可用
2.open camera
3.設定preview引數
4.start preview
5.takePicture
take picture start
take picture done
6.回到1
二.Preview設定
preview的設定可以通過設定Camera.Parameters來完成。
以下是Parameters的可設定內容:
除此之外,通過Camera例項還可以設定相機的自動對焦,顯示角度調整等。
Camera.Parameters parameters = camera.getParameters();
//設定最大幀率
List<int[]> fpsList = parameters.getSupportedPreviewFpsRange();
if (fpsList != null && fpsList.size() > 0) {
int[] maxFps = fpsList.get(0);
for (int[] fps : fpsList) {
if (maxFps[0] * maxFps[1] < fps[0] * fps[1]) {
maxFps = fps;
}
}
parameters.setPreviewFpsRange (maxFps[0], maxFps[1]);
Log.d("yuanj", maxFps[0] + " " + maxFps[1]);
}
//設定Preview解析度
parameters.setPreviewSize(1920, 1080);
//設定preview的orientation,與下面的setDisplayOrientation一起用
parameters.set("orientation", "portrait");
...
//使設定引數生效
camera.setParameters(parameters);
//設定顯示旋轉90度,一般的camera都是顯示為逆時針90度
camera.setDisplayOrientation(90);
//設定自動對焦
camera.setAutoFocusMoveCallback(new Camera.AutoFocusMoveCallback() {
@Override
public void onAutoFocusMoving(boolean start, Camera camera) {
}
});
三.Camera 預覽幀獲取/顯示
Camera預覽幀的獲取/顯示目前有兩種方式。通過native window將資料給surface holder完成顯示或使用Preview Callback的回撥方法onPreviewFrame來獲取幀資料。
1.Native window
這種方式只能進行幀資料顯示,無法對預覽幀進行後續處理,做預覽特效一般使用Callback方式。但是這種方式是直接取得資料,幀數高且無預覽畫面旋轉問題。
2.Preview Callback
此種方式需要在回撥中進行每一幀的處理和繪畫,因此效能方面會有一定影響。
進行預覽幀處理可以使用GLSurfaceView,來做出自定義預覽畫面。
本次例項使用基礎View SurfaceView來實現基本的preview功能。
四.預覽幀顯示功能實現
1.獲取Camera許可權(版本高記得申請許可權)
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.WEITE_EXTERNAL_STORAGE" />
2.建立SurfaceView並設定Callback
surfaceView = findViewById(R.id.surface);
surfaceView.getHolder().addCallback(this);
3.openCamera
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
mCamera = Camera.open();
try {
mCamera.setPreviewDisplay(holder);
} catch (Exception e) {
mCamera.release();
mCamera = null;
}
}
4.設定Perview引數並開始Preview
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
Camera.Parameters parameters = camera.getParameters();
//設定最大幀率範圍
List<int[]> fpsList = parameters.getSupportedPreviewFpsRange();
if (fpsList != null && fpsList.size() > 0) {
int[] maxFps = fpsList.get(0);
for (int[] fps : fpsList) {
if (maxFps[0] * maxFps[1] < fps[0] * fps[1]) {
maxFps = fps;
}
}
parameters.setPreviewFpsRange(maxFps[0], maxFps[1]);
Log.d("Test", maxFps[0] + " " + maxFps[1]);
}
// parameters.setRecordingHint(true); 強制開啟Preview
parameters.setPreviewSize(1920, 1080);
parameters.set("orientation", "portrait");
mCamera.setDisplayOrientation(90);
mCamera.setParameters(parameters);
mCamera.startPreview();
}
五.預覽幀獲取並顯示
在採用第一種方式的情況下,可以通過Callback獲取每一幀的資料。
有一些特殊需求如Preview原畫面和載入特效畫面的比較可以通過Callback在另一個Surface上畫特效。
這裡只使用一個Surface完成預覽畫面的顯示,因此不能將holder傳遞下去。
需要在surfaceCreated時進行以下修改:
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
mCamera = Camera.open();
try {
//這裡的mSurfaceTexture是成員變數,防止引用被回收
//感興趣的話,這裡的surfaceTexture可以直接用 new SurfaceTexture(0)嘗試一下
mCamera.setPreviewTexture(mSurfaceTexture);
} catch (Exception e) {
mCamera.release();
mCamera = null;
}
}
在設定Preview引數時設定PreviewCallback
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
Camera.Parameters parameters = camera.getParameters();
List<int[]> fpsList = parameters.getSupportedPreviewFpsRange();
if (fpsList != null && fpsList.size() > 0) {
int[] maxFps = fpsList.get(0);
for (int[] fps : fpsList) {
if (maxFps[0] * maxFps[1] < fps[0] * fps[1]) {
maxFps = fps;
}
}
parameters.setPreviewFpsRange(maxFps[0], maxFps[1]);
Log.d("Test", maxFps[0] + " " + maxFps[1]);
}
// 設定幀率30
// parameters.setPreviewFrameRate(30);
// parameters.setRecordingHint(true);
parameters.setPreviewSize(1920, 1080);
//新增Preview Callback
PreviewCallback previewCallback = new PreviewCallback();
mCamera.setPreviewCallback(previewCallback);
parameters.set("orientation", "portrait");
mCamera.setDisplayOrientation(90);
mCamera.setParameters(parameters);
mCamera.startPreview();
}
在回撥中獲取資料,解析成Bitmap後繪製到Surface上
private class PreviewCallback implements Camera.PreviewCallback {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
Camera.Size size = camera.getParameters().getPreviewSize();//獲得預覽影象設定的尺寸
YuvImage img = new YuvImage(data, ImageFormat.NV21, size.width, size.height, null);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
img.compressToJpeg(new Rect(0, 0, size.width, size.height), 80, stream);
Bitmap bitmap = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
try {
stream.close();
} catch (IOException e) {
e.printStackTrace();
}
Canvas canvas = surfaceView.getHolder().lockCanvas();
canvas.drawBitmap(bitmap, null, getImageRect(bitmap.getWidth(), bitmap.getHeight()), new Paint());
bitmap.recycle();
surfaceView.getHolder().unlockCanvasAndPost(canvas);
}
}
六.回撥方式效能優化
原生camera設定PreviewCallback的方式有三種:
PreviewCallback previewCallback = new PreviewCallback();
camera.setPreviewCallbackWithBuffer(previewCallback);
camera.setOneShotPreviewCallback(previewCallback);
camera.setPreviewCallback(previewCallback);
這裡做效能優化使用第一種方式。
為什麼能優化呢?
這是基於底層傳遞資料的方式:
無buffer設定時,底層每捕捉到一幀資料,就需要new一個buffer來進行傳遞。每次傳遞結束,由於回收機制,都需要將剛剛new的buffer進行回收,這樣就不斷new一個buffer並進行回收。
而當有buffer時,底層會有一個Vector來存放buffer。當Vector不為空時,底層才會使用Vector中的空閒buffer,當Vector為空時,底層則會切換為無buffer設定情況。
說到這裡,可能會奇怪,為什麼Vector會空呢?
每次通過Vector中的buffer傳遞資料,都會將該buffer從Vector中移除。如果不往Vector中再新增buffer,自然Vector會空。
如何新增buffer呢?
通過addCallbackBuffer來新增。
mCamera.addCallbackBuffer(new byte[size]);
何時新增buffer呢?
開始preview時,底層需要buffer傳遞,因此在preview前需要新增;
當Callback返回資料後,承載資料的buffer已經可以釋放,這時候需要將該buffer添加回去。
那麼buffer 的size應該如何定呢?
buffer的size通過previewSize來計算,計算公式為width * height * 3 / 2
parameters.setPreviewSize(1920, 1080);
// 一般3個buffer是肯定夠的
for (int i = 0; i < 3; i++) {
// 這裡3110400 = 1920 * 1080 *3 /2
mCamera.addCallbackBuffer(new byte[3110400]);
}
PreviewCallback previewCallback = new PreviewCallback();
mCamera.setPreviewCallbackWithBuffer(previewCallback);
關於解析資料,這裡的data是YUV格式的資料,直接使用Bitmap轉換是不行的,會報空指標,需要使用YuvImage來轉。
但是在回撥裡頭做資料處理肯定會影響效能,於是我想到用Handler將data轉移出來做處理。
private class PreviewCallback implements Camera.PreviewCallback {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
Message message = Message.obtain();
message.obj = data;
mHandler.sendMessage(message);
camera.addCallbackBuffer(data);
}
}
在Handler中進行資料處理,可以感受到幀率有一定提升。
但是也會有缺點,當裝置效能不足時會出現跳幀,丟幀問題。
private class DecodeDataHandler extends Handler {
@Override
public void handleMessage(@NonNull Message msg) {
byte[] data = (byte[]) msg.obj;
Camera.Size size = camera.getParameters().getPreviewSize();//獲得預覽影象設定的尺寸
//關於ImageFormat,目前只支援NV21和YUY2,其他的會報錯
YuvImage img = new YuvImage(data, ImageFormat.NV21, size.width, size.height, null);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
// 80表示轉Jpeg的質量,最高100,會影響到預覽效能,越高幀率越低
img.compressToJpeg(new Rect(0, 0, size.width, size.height), 80, stream);
Bitmap bitmap = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
try {
stream.close();
} catch (IOException e) {
e.printStackTrace();
}
Canvas canvas = surfaceView.getHolder().lockCanvas();
canvas.drawBitmap(bitmap, null, getImageRect(bitmap.getWidth(), bitmap.getHeight()), new Paint());
bitmap.recycle();
surfaceView.getHolder().unlockCanvasAndPost(canvas);
}
}
七.遺留問題
回撥處理時,返回的資料是逆時針90度的,無論是否使用了setDisplayOrientation。
我嘗試在回撥處理時進行Image的90度偏轉,會極大的影響幀率表現。
偏轉處理嘗試的方法包括YUV資料轉換和Bitmap資料轉換,甚至嘗試了ObjcetAnimator的偏轉動畫,結果都不盡人意。
基礎的SurfaceView可能解決不了該問題,使用GL執行緒的GLSurfaceView有望解決,該問題留待後續解決。
大家如果有什麼好的建議,歡迎留言!