Android Camera 开发你该知道的秘密㊙️-新手入门必备
作者:@魷魚先生 本文為原創,轉載請注明:juejin.im/user/5aff97…
安卓相機相關開發的文章已經數不勝數,今天提筆想給開發者說說安卓相機開發的一些小秘密,當然也會進行一些基礎知識的普及?。如果還沒有相機開發相關支持的小伙伴,建議打開谷歌的文檔 Camera 和 Camera Guide 進行相關的學習,然后再結合本文的內容,一定可以達到事倍功半的效果。
這里提前附上參考代碼的克隆地址: ps: ?貼心的博主特地使用碼云方便國內的小伙伴們高速訪問代碼。
碼云:Camera-Android
本文主要是介紹安卓Camera1相關的介紹,Camera2的就等待我的更新吧:)?
1. 啟動相機
從API文檔和很多網絡的資料一般的啟動套路代碼:
/** A safe way to get an instance of the Camera object. */ public static Camera getCameraInstance(){Camera c = null;try {c = Camera.open(); // attempt to get a Camera instance}catch (Exception e){// Camera is not available (in use or does not exist)}return c; // returns null if camera is unavailable } 復制代碼但是調用該函數獲取相機實例的時候,一般調用都是直接在 MainThread 中直接調用該函數:
protected void onCreate(Bundle savedInstanceState) {// ... Camera camera = getCameraInstance();} 復制代碼讓我們來看看安卓源碼的是實現,Camera.java:
/*** Creates a new Camera object to access the first back-facing camera on the* device. If the device does not have a back-facing camera, this returns* null.* @see #open(int)*/ public static Camera open() {int numberOfCameras = getNumberOfCameras();CameraInfo cameraInfo = new CameraInfo();for (int i = 0; i < numberOfCameras; i++) {getCameraInfo(i, cameraInfo);if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) {return new Camera(i);}}return null; }Camera(int cameraId) {mShutterCallback = null;mRawImageCallback = null;mJpegCallback = null;mPreviewCallback = null;mPostviewCallback = null;mUsingPreviewAllocation = false;mZoomListener = null;Looper looper;if ((looper = Looper.myLooper()) != null) {mEventHandler = new EventHandler(this, looper);} else if ((looper = Looper.getMainLooper()) != null) {mEventHandler = new EventHandler(this, looper);} else {mEventHandler = null;}String packageName = ActivityThread.currentPackageName();native_setup(new WeakReference<Camera>(this), cameraId, packageName); } 復制代碼注意mEventHandler如果當前的啟動線程不帶 Looper 則默認的 mEventHandler 使用UI線程的默認 Looper。從源碼我們可以看到 EventHandler 負責處理底層的消息的回調。正常情況下,我們期望所有回調都在UI線程這樣可以方便我們直接操作相關的頁面邏輯。但是針對一些特殊場景我們可以做一些特殊的操作,目前可以把這個知識點記下,以便后續他用。
2. 設置相機?預覽模式
2.1 使用 SurfaceHolder 預覽
根據官方的 Guide 文章我們直接使用 SurfaceView 作為預覽的展示對象。
protected void onCreate(Bundle savedInstanceState) {// ...SurfaceView surfaceView = findViewById(R.id.camera_surface_view);surfaceView.getHolder().addCallback(this); } public void surfaceCreated(SurfaceHolder holder) {// TODO: Connect Camera.if (null != mCamera) {try {mCamera.setPreviewDisplay(holder);mCamera.startPreview();mHolder = holder;} catch (IOException e) {e.printStackTrace();}} } 復制代碼重新運行下程序,我相信你已經可以看到預覽的畫面,當然它可能有些方向的問題。但是我們至少看到了相機的畫面。
2.2 使用 SurfaceTexture 預覽
該方式目前主要是針對需要利用 OpenGL ES 作為相機 GPU 預覽的模式。此時使用的目標 View 也換成了 GLSurfaceView。在使用的時候??注意3個小細節:
關于被動刷新的開啟,第三點會詳細介紹它的意思。 2. 創建紋理對應的 SurfaceTexture
public void onSurfaceCreated(GL10 gl, EGLConfig config) {// Init Cameraint[] textureIds = new int[1];GLES20.glGenTextures(1, textureIds, 0);GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureIds[0]);// 超出紋理坐標范圍,采用截斷到邊緣GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);//過濾(紋理像素映射到坐標點) (縮小、放大:GL_LINEAR線性)GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);mSurfaceTexture = new SurfaceTexture(textureIds[0]);mCameraTexture = textureIds[0];GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);try {// 創建的 SurfaceTexture 作為預覽用的 TexturemCamera.setPreviewTexture(mSurfaceTexture); mCamera.startPreview();} catch (IOException e) {e.printStackTrace();} } 復制代碼這里創建的紋理是一種特殊的來自 OpenGL ES 的擴展,GLES11Ext.GL_TEXTURE_EXTERNAL_OES 有且只有在使用此種類型紋理的時候,開發者才能通過自己的 GPU 代碼進行攝像頭內容的實時處理。 3. 數據驅動刷新
將原有的 GLSurfaceView 連續刷新的模式改成,只有當數據有變化的時候才刷新。
GLSurfaceView surfaceView = findViewById(R.id.gl_surfaceview); surfaceView.setEGLContextClientVersion(2); surfaceView.setRenderer(this); // 添加以下設置,改成被動的 GL 渲染。 // Change SurfaceView render mode to RENDERMODE_WHEN_DIRTY. surfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); 復制代碼當數據變化的時候我們可以通過以下方式進行通知
mSurfaceTexture.setOnFrameAvailableListener(surfaceTexture -> {// 有數據可以進行展示,同時GL線程工作。mSurfaceView.requestRender(); }); 復制代碼其余的部分可以不變,這樣的好處是刷新的幀率可以隨著相機的幀率變化而變化。不是自己一直自動刷新造成不必要的GPU功耗。
2.3 使用YUV-NV21 預覽
本節將重點介紹如何使用YUV數據進行相機的畫面的預覽的技術實現。這個技術方案主要的落地場景是 人臉識別(Face Detection) 或是其他 CV 領域的實時算法數據加工。
2.3.1 設置回調 Camera 預覽 YUV 數據回調 Buffer
本步驟利用舊版本的接口 Camera.setPreviewCallbackWithBuffer , 但是使用此函數需要做一個必要操作,就是往相機里面添加回調數據的 Buffer。
// 設置目標的預覽分辨率,可以直接使用 1280*720 目前的相機都會有該分辨率 parameters.setPreviewSize(previewSize.first, previewSize.second); // 設置相機 NV21 數據回調使用用戶設置的 buffer mCamera.setPreviewCallbackWithBuffer(this); mCamera.setParameters(parameters); // 添加4個用于相機進行處理的 byte[] buffer 對象。 mCamera.addCallbackBuffer(createPreviewBuffer(previewSize.first, previewSize.second)); mCamera.addCallbackBuffer(createPreviewBuffer(previewSize.first, previewSize.second)); mCamera.addCallbackBuffer(createPreviewBuffer(previewSize.first, previewSize.second)); mCamera.addCallbackBuffer(createPreviewBuffer(previewSize.first, previewSize.second)); 復制代碼這里需要注意??,如果設置預覽回調使用的是 Camera.setPreviewCallback 那么相機返回的數據 onPreviewFrame(byte[] data, Camera camera) 中的 data 是由相機內部創建。
public void onPreviewFrame(byte[] data, Camera camera) {// TODO: 預處理相機輸入數據if (!bytesToByteBuffer.containsKey(data)) {Log.d(TAG, "Skipping frame. Could not find ByteBuffer associated with the image "+ "data from the camera.");} else {// 因為我們使用的是 setPreviewCallbackWithBuffer 所以必須把data還回去mCamera.addCallbackBuffer(data);} } 復制代碼如果不進行 mCamera.addCallbackBuffer(byte[]), 當回調 4 次之后,就不會再觸發 onPreviewFrame 。可以發現次數剛好等于相機初始化時候添加的 Buffer 個數。
2.3.2 啟動相機預覽
我們目的是使用 onPreviewFrame 返回數據進行渲染,所以設置 mCamera.setPreviewTexture 的邏輯代碼需要去除,因為我們不希望相機還繼續把預覽的數據繼續發送給之前設置的 SurfaceTexture 這個就系統浪費資源了。
?支持注釋相機 mCamera.setPreviewTexture(mSurfaceTexture); 的代碼段:
try {// mCamera.setPreviewTexture(mSurfaceTexture);mCamera.startPreview(); } catch (Exception e) {e.printStackTrace(); } 復制代碼通過測試發現 onPreviewFrame 居然不工作了,快速看下文檔,里面提到以下信息:
/*** Starts capturing and drawing preview frames to the screen* Preview will not actually start until a surface is supplied* with {@link #setPreviewDisplay(SurfaceHolder)} or* {@link #setPreviewTexture(SurfaceTexture)}.** <p>If {@link #setPreviewCallback(Camera.PreviewCallback)},* {@link #setOneShotPreviewCallback(Camera.PreviewCallback)}, or* {@link #setPreviewCallbackWithBuffer(Camera.PreviewCallback)} were* called, {@link Camera.PreviewCallback#onPreviewFrame(byte[], Camera)}* will be called when preview data becomes available.** @throws RuntimeException if starting preview fails; usually this would be* because of a hardware or other low-level error, or because release()* has been called on this Camera instance.*/ public native final void startPreview(); 復制代碼相機的有且僅有被設置的對應的 Surface 資源之后才能正確的啟動預覽。
下面是見證奇跡的時刻了:
/*** The dummy surface texture must be assigned a chosen name. Since we never use an OpenGL context,* we can choose any ID we want here. The dummy surface texture is not a crazy hack - it is* actually how the camera team recommends using the camera without a preview.*/ private static final int DUMMY_TEXTURE_NAME = 100; public void onSurfaceCreated(GL10 gl, EGLConfig config) {// ... codesSurfaceTexture dummySurfaceTexture = new SurfaceTexture(DUMMY_TEXTURE_NAME);mCamera.setPreviewTexture(dummySurfaceTexture);// ... codes } 復制代碼這個操作之后,相機的 onPreviewFrame 又開始被觸發了。這個虛擬的 SurfaceTexture 它可以讓相機工作起來,并且通過設置 :
dummySurfaceTexture.setOnFrameAvailableListener(surfaceTexture -> {Log.d(TAG, "dummySurfaceTexture working.");}); 復制代碼我們會發現系統是能自己判斷出 SurfaceTexture 是否有效,接著 onFrameAvailable 也毫無反應。
2.3.3 渲染 YUV 數據繪制到 SurfaceView。
目前安卓默認的YUV格式是 NV21. 所以需要使用 Shader 進行格式的轉換。 在 OpenGL 中只能進行 RGB 的顏色進行繪制。具體腳本算法可以參考: nv21_to_rgba_fs.glsl
precision highp float; varying vec2 v_texCoord; uniform sampler2D y_texture; uniform sampler2D uv_texture;void main (void) {float r, g, b, y, u, v;//We had put the Y values of each pixel to the R,G,B components by//GL_LUMINANCE, that's why we're pulling it from the R component,//we could also use G or By = texture2D(y_texture, v_texCoord).r;//We had put the U and V values of each pixel to the A and R,G,B//components of the texture respectively using GL_LUMINANCE_ALPHA.//Since U,V bytes are interspread in the texture, this is probably//the fastest way to use them in the shaderu = texture2D(uv_texture, v_texCoord).a - 0.5;v = texture2D(uv_texture, v_texCoord).r - 0.5;//The numbers are just YUV to RGB conversion constantsr = y + 1.13983*v;g = y - 0.39465*u - 0.58060*v;b = y + 2.03211*u;//We finally set the RGB color of our pixelgl_FragColor = vec4(r, g, b, 1.0); } 復制代碼主要思路是將N21的數據直接分離成2張紋理數據,fragment shader 里面進行顏色格式的計算,算回 RGBA。
mYTexture = new Texture(); created = mYTexture.create(mYuvBufferWidth, mYuvBufferHeight, GLES10.GL_LUMINANCE); if (!created) {throw new RuntimeException("Create Y texture fail."); }mUVTexture = new Texture(); created = mUVTexture.create(mYuvBufferWidth/2, mYuvBufferHeight/2, GLES10.GL_LUMINANCE_ALPHA); // uv 因為是兩個通道所以數據的格式上選擇 GL_LUMINANCE_ALPHA if (!created) {throw new RuntimeException("Create UV texture fail."); }// ...省略部分邏輯代碼//Copy the Y channel of the image into its buffer, the first (width*height) bytes are the Y channel yBuffer.put(data.array(), 0, mPreviewSize.first * mPreviewSize.second); yBuffer.position(0);//Copy the UV channels of the image into their buffer, the following (width*height/2) bytes are the UV channel; the U and V bytes are interspread uvBuffer.put(data.array(), mPreviewSize.first * mPreviewSize.second, (mPreviewSize.first * mPreviewSize.second)/2); uvBuffer.position(0);mYTexture.load(yBuffer); mUVTexture.load(uvBuffer); 復制代碼2.3.4 性能優化
相機的回調 YUV 的速度和 OpenGL ES 渲染相機預覽畫面的速度不一定是匹配的,所以我們可以進行優化。既然是相機的預覽我們必須保證當前渲染的畫面一定是最新的。我們可以利用 pendingFrameData 一個公用資源進行渲染線程和相機數據回調線程的同步,保證畫面的時效性。
synchronized (lock) {if (pendingFrameData != null) { // frame data tha has not been processed. Just return back to Camera.camera.addCallbackBuffer(pendingFrameData.array());pendingFrameData = null;}pendingFrameData = bytesToByteBuffer.get(data);// Notify the processor thread if it is waiting on the next frame (see below).// Demo 中是通知 GLThread 中渲染線程如果處理等待狀態就是直接喚醒。lock.notifyAll(); }// 通知 GLSurfaceView 可以刷新了 mSurfaceView.requestRender(); 復制代碼最后還有一個優化的小技巧秘?,需要結合在 啟動相機 中提到的關于 Handler 的事情。如果我們是在安卓的主線程或是不帶有 Looper 的子線程中調用相機 Camera.open() 最終的結局都是所有相機的回調信息都會從主線程的 Looper.getMainLooper() 的 Looper 進行信息處理。我們可以想象如果目前 UI 的線程正在進行重的操作,勢必將影響到相機預覽的幀率問題,所以最好的方法就是開辟子線程進行相機的開啟操作。
final ConditionVariable startDone = new ConditionVariable();new Thread() {public void run() {Log.v(TAG, "start loopRun");// Set up a looper to be used by camera.Looper.prepare();// Save the looper so that we can terminate this thread// after we are done with it.mLooper = Looper.myLooper();mCamera = Camera.open(cameraId);Log.v(TAG, "camera is opened");startDone.open();Looper.loop(); // Blocks forever until Looper.quit() is called.if (LOGV) Log.v(TAG, "initializeMessageLooper: quit.");} }.start();Log.v(TAG, "start waiting for looper");if (!startDone.block(WAIT_FOR_COMMAND_TO_COMPLETE)) {Log.v(TAG, "initializeMessageLooper: start timeout");fail("initializeMessageLooper: start timeout"); } 復制代碼3. 攝像頭角度問題
攝像頭的數據預覽是跟攝像頭傳感器的安裝位置有關系的,相關的內容可以單獨再寫一篇文章進行討論,我這邊就直接上代碼。
private void setRotation(Camera camera, Camera.Parameters parameters, int cameraId) {WindowManager windowManager = (WindowManager)getSystemService(Context.WINDOW_SERVICE);int degrees = 0;int rotation = windowManager.getDefaultDisplay().getRotation();switch (rotation) {case Surface.ROTATION_0:degrees = 0;break;case Surface.ROTATION_90:degrees = 90;break;case Surface.ROTATION_180:degrees = 180;break;case Surface.ROTATION_270:degrees = 270;break;default:Log.e(TAG, "Bad rotation value: " + rotation);}Camera.CameraInfo cameraInfo = new Camera.CameraInfo();Camera.getCameraInfo(cameraId, cameraInfo);int angle;int displayAngle;if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {angle = (cameraInfo.orientation + degrees) % 360;displayAngle = (360 - angle) % 360; // compensate for it being mirrored} else { // back-facingangle = (cameraInfo.orientation - degrees + 360) % 360;displayAngle = angle;}// This corresponds to the rotation constants.mRotation = angle;camera.setDisplayOrientation(displayAngle);parameters.setRotation(angle); } 復制代碼但是測試中你會發現在使用YUV數據預覽模式的時候是不起作用的,這個是因為設置的角度參數不會直接影響 PreviewCallback#onPreviewFrame 返回的結果。我們通過查看源碼的注釋后更加確信這點。
/*** Set the clockwise rotation of preview display in degrees. This affects* the preview frames and the picture displayed after snapshot. This method* is useful for portrait mode applications. Note that preview display of* front-facing cameras is flipped horizontally before the rotation, that* is, the image is reflected along the central vertical axis of the camera* sensor. So the users can see themselves as looking into a mirror.** <p>This does not affect the order of byte array passed in {@link* PreviewCallback#onPreviewFrame}, JPEG pictures, or recorded videos. This* method is not allowed to be called during preview.** <p>If you want to make the camera image show in the same orientation as* the display, you can use the following code.* <pre>* public static void setCameraDisplayOrientation(Activity activity,* int cameraId, android.hardware.Camera camera) {* android.hardware.Camera.CameraInfo info =* new android.hardware.Camera.CameraInfo();* android.hardware.Camera.getCameraInfo(cameraId, info);* int rotation = activity.getWindowManager().getDefaultDisplay()* .getRotation();* int degrees = 0;* switch (rotation) {* case Surface.ROTATION_0: degrees = 0; break;* case Surface.ROTATION_90: degrees = 90; break;* case Surface.ROTATION_180: degrees = 180; break;* case Surface.ROTATION_270: degrees = 270; break;* }** int result;* if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {* result = (info.orientation + degrees) % 360;* result = (360 - result) % 360; // compensate the mirror* } else { // back-facing* result = (info.orientation - degrees + 360) % 360;* }* camera.setDisplayOrientation(result);* }* </pre>** <p>Starting from API level 14, this method can be called when preview is* active.** <p><b>Note: </b>Before API level 24, the default value for orientation is 0. Starting in* API level 24, the default orientation will be such that applications in forced-landscape mode* will have correct preview orientation, which may be either a default of 0 or* 180. Applications that operate in portrait mode or allow for changing orientation must still* call this method after each orientation change to ensure correct preview display in all* cases.</p>** @param degrees the angle that the picture will be rotated clockwise.* Valid values are 0, 90, 180, and 270.* @throws RuntimeException if setting orientation fails; usually this would* be because of a hardware or other low-level error, or because* release() has been called on this Camera instance.* @see #setPreviewDisplay(SurfaceHolder)*/public native final void setDisplayOrientation(int degrees); 復制代碼為了得到正確的方向角度。我們需要進行YUV渲染的是改變下坐標點。 這里我用了一個很暴力的手段,直接去調整下紋理的坐標
private static final float FULL_RECTANGLE_COORDS[] = {-1.0f, -1.0f, // 0 bottom left1.0f, -1.0f, // 1 bottom right-1.0f, 1.0f, // 2 top left1.0f, 1.0f, // 3 top right};// FIXME: 為了繪制正確的角度,將紋理坐標按90度進行計算,中間還包含了一次紋理數據的鏡像處理private static final float FULL_RECTANGLE_TEX_COORDS[] = {1.0f, 1.0f, // 0 bottom left1.0f, 0.0f, // 1 bottom right0.0f, 1.0f, // 2 top left0.0f, 0.0f // 3 top right}; 復制代碼重啟程序 Perfect 搞定。
總結
關于安卓相機的開發,總結就是在踩坑中度過。建議正在學習的同學,最好能結合我參考資料里面附加的內容以及相機源碼進行學習。你將會得到很大的收獲。 同時我也希望自己寫的經驗文章可以幫到正在學習的你。???
參考資料
轉載于:https://juejin.im/post/5c924d8bf265da60f30d45c7
總結
以上是生活随笔為你收集整理的Android Camera 开发你该知道的秘密㊙️-新手入门必备的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Airbnb(爱彼迎)用户数据分析——t
- 下一篇: android sina oauth2.