Android Camera 踩坑

標(biāo)簽(空格分隔):Android Camera 相機(jī) 圖像方向 圖像大小

【注】本文所提到的 Camera 均為 android.hardware 這個(gè)包下的 Camera


在開發(fā)自定義 Camera 時(shí),經(jīng)常會(huì)遇見各種在方向和大小上的問題。例如:預(yù)覽的方向顛倒了、預(yù)覽的圖像被拉升了、拍攝出來的照片與預(yù)覽時(shí)的圖像不一致等等。這些問題的主要原因是對(duì)于Camera 中有關(guān)方向和大小的幾個(gè)概念沒有理解。本文系統(tǒng)的梳理這些知識(shí),同時(shí)有誤之處歡迎大家留言以提醒其它的讀者。

1. 方向

1.1 相機(jī)傳感器方向

這里的相機(jī)傳感器有的博客中也叫圖像傳感器(Image Sensor)這里以官方文檔中給出名詞(Camera Sensor)為準(zhǔn)。
在 Camera#setRotation 方法的注釋中有這樣一段話:

Suppose a back-facing camera sensor is mounted inlandscape and the top side of the camera sensor is aligned with the right edge of the display in natural orientation
假設(shè)后置相機(jī)的傳感器是被水平安裝的,并且相機(jī)傳感器的頂部與自然狀態(tài)下手機(jī)的右邊緣對(duì)齊。

一圖勝千言:


圖1 相機(jī)傳感器的位置
圖1 相機(jī)傳感器的位置

其實(shí),這段話中還包含著一個(gè)隱藏信息。即,相機(jī)傳感器獲得的圖像是水平的且寬大于高。舉例來說,一個(gè)4:3的800萬像素的傳感器在水平方向上有3264個(gè)像素,在豎直方向上有2448個(gè)像素。即,800w ≈ 3264*2448。這樣的一張圖片就是我們通過相機(jī)傳感器獲得的最原始的圖片。

當(dāng)我們讓相機(jī)傳感器分別位于手機(jī)的左上角、右上角、右下角、左下角時(shí)我們采集到的原始圖像分別如下圖所示:


圖2 相機(jī)傳感器方向與所采集到的圖像方向
圖2 相機(jī)傳感器方向與所采集到的圖像方向

1.2 相機(jī)預(yù)覽方向

1.2.1 什么是相機(jī)的預(yù)覽方向:

【注】這里的圖像指的是我們通過 SurfaceView 或者 TextureView 所看到的,在手機(jī)屏幕上呈現(xiàn)出來的圖像。

1.2.2 如何設(shè)置預(yù)覽方向:

當(dāng)我們拿到相機(jī)傳感器所采集到的圖像后,我們需要對(duì)圖像進(jìn)行變換以符合我們直觀的視覺感受。由于采集到的圖像信息是通過 SurfaceView 或者 TextureView 顯示的,所以我們只需要控制好這兩個(gè) View 的方向即可。

對(duì)于 SurfaceView,由于其不在 View hierachy 中,因此我們不能簡(jiǎn)單的對(duì)其進(jìn)行選擇變換操作。而是應(yīng)該在調(diào)用完 Camera#setPreviewDisplay 方法綁定好 SurfaceHolder 之后。再通過 Camera#setDisplayOrientation 來改變其方向。

對(duì)于 TextureView 由于其作為 View hierachy 中的一個(gè)普通 View。因此我們可以通過操作 matrix ,最后再調(diào)用 TextureView#setTransform 來改變其方向;

1.2.3 如何確定預(yù)覽方向:

我們直接分析一下,Camera#setDisplayOrientation 的注釋中所給出的方案。

//If you want to make the camera image show in the same orientation as the display, you can use the following code.
 public static void setCameraDisplayOrientation(Activity activity, int cameraId, android.hardware.Camera camera) {
        //通過相機(jī)ID獲得相機(jī)信息      
        android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
        android.hardware.Camera.getCameraInfo(cameraId, info);

        //獲得當(dāng)前屏幕方向   
        int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
        int degrees = 0;
        switch (rotation) {
            //若屏幕方向與水平軸負(fù)方向的夾角為0度       
            case Surface.ROTATION_0:
                degrees = 0;
                break;
            //若屏幕方向與水平軸負(fù)方向的夾角為90度       
            case Surface.ROTATION_90:
                degrees = 90;
                break;
            //若屏幕方向與水平軸負(fù)方向的夾角為180度       
            case Surface.ROTATION_180:
                degrees = 180;
                break;
            //若屏幕方向與水平軸負(fù)方向的夾角為270度   
            case Surface.ROTATION_270:
                degrees = 270;
                break;
        }
        int result;
        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            //前置攝像頭作鏡像翻轉(zhuǎn)      
            result = (info.orientation + degrees) % 360;
            result = (360 - result) % 360;  // compensate the mirror   
        } else {  // back-facing       
            result = (info.orientation - degrees + 360) % 360;
        }
        camera.setDisplayOrientation(result);
    }

從上述代碼可以看出,相機(jī)的預(yù)覽方向與以下3個(gè)因素有關(guān):
1.屏幕方向:orientation
2.相機(jī)圖像方向: (orientation of the camera image):在 Camera 中對(duì)應(yīng)的是CameraInfo.orientation ,在 Camera2 中對(duì)應(yīng)的是 CameraCharacteristics.SENSOR_ORIENTATION。
3.相機(jī)是前置還是后置:CAMERA_FACING_FRONT/CAMERA_FACING_BACK

對(duì)于屏幕方向:
在開啟屏幕自動(dòng)旋轉(zhuǎn)的狀態(tài)下:自然握持狀態(tài)下為0度,逆時(shí)針旋狀依次為:90度、180度(有的手機(jī)沒有這個(gè)角度)、270度;
在鎖定屏幕方向的狀態(tài)下:均為0度。

對(duì)于相機(jī)圖像方向:
先來看下官方文檔

The orientation of the camera image. The value is the angle that the camera image needs to be rotated clockwise so it shows correctly onthe display in its natural orientation. It should be 0, 90, 180, or 270.
For example, suppose a device has a naturally tall screen. The back-facing camera sensor is mounted in landscape. You are looking at the screen. If the top side of the camera sensor is aligned with the right edge of the screen in natural orientation, the value should be 90. If the top side of a front-facing camera sensor is aligned with the right of the screen, the value should be 270.

簡(jiǎn)單的總結(jié)下:
1.相機(jī)的圖像方向即是相機(jī)圖像需要順時(shí)針旋轉(zhuǎn)的角度,以便讓圖像以正確的角度呈現(xiàn)。這個(gè)角度應(yīng)該是0、90、180、270度。
2.舉例來說,當(dāng)使用后置攝像頭時(shí),這個(gè)角度是90度。當(dāng)使用前置攝像頭時(shí),這個(gè)角度時(shí)270度。在我手上的測(cè)試機(jī)器中沒有發(fā)現(xiàn)0度和180度的情況(猜想:這兩種角度可能在那些攝像頭非固定的手機(jī)上會(huì)出現(xiàn))。

這里我暫時(shí)沒有想到為何這樣去計(jì)算相機(jī)的預(yù)覽方向,但是既然原理我們已經(jīng)理解了,那么我們可以從結(jié)果入手分析。


圖3 不同屏幕方向及相機(jī)圖像方向下對(duì)應(yīng)的 display orientation 的值
圖3 不同屏幕方向及相機(jī)圖像方向下對(duì)應(yīng)的 display orientation 的值

在屏幕方向鎖定的情況下,由于 rotation 的值始終為0,因此上面給出的計(jì)算相機(jī)預(yù)覽方向的方法就不適用了。下面我們來分析這種相對(duì)來說較為簡(jiǎn)單的情況:
1.在鎖定豎屏的情況下:此時(shí) SurfaceView/TextureView 的寬小于高,而相機(jī)傳感器采集到的圖像寬大于高,故需要旋轉(zhuǎn)90即可。
2.在鎖定橫屏的情況下:此時(shí) SurfaceView/TextureView 的寬大于高,與相機(jī)傳感器采集到的圖像的寬高大小關(guān)系相同,故設(shè)置為0度即可。

1.3 圖片預(yù)覽方向

1.3.1什么是圖片的預(yù)覽方向:

這里的圖像指的是我們通過 ImageView 或類似的用來顯示 Bitmap 的控件所顯示的,并最終能以 jpg 之類的格式保存起來的圖像。

如何設(shè)置圖片的預(yù)覽方向:
當(dāng)使用 Camera 類時(shí),我門可以通過 Camera.Parameters#setRotation(jpegOrientation) 方法來設(shè)置圖片的預(yù)覽方向。當(dāng)使用 Camera2 類時(shí),我們則可以通過 CaptureRequest.Builder#set(CaptureRequest.JPEG_ORIENTATION, jpegOrientation)方法來設(shè)置圖片的預(yù)覽方向。

1.3.2 如何確定圖片的預(yù)覽方向:

我們不妨還是先來看一下 Camera.Parameters#setRotation 方法的注釋中所給出的方案。

For example, suppose the natural orientation of the device isportrait. The device is rotated 270 degrees clockwise, so the deviceorientation is 270. Suppose a back-facing camera sensor is mounted inlandscape and the top side of the camera sensor is aligned with theright edge of the display in natural orientation. So the cameraorientation is 90. The rotation should be set to 0 (270 + 90).

 public void onOrientationChanged(int orientation) {
        if (orientation == ORIENTATION_UNKNOWN) return;
        android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
        android.hardware.Camera.getCameraInfo(cameraId, info);
        // Round device orientation to a multiple of 90  
        orientation = (orientation + 45) / 90 * 90;   
        int rotation = 0;
        // Reverse device orientation for front-facing cameras   
        if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
            rotation = (info.orientation - orientation + 360) % 360;
        } else {  // back-facing camera        
            rotation = (info.orientation + orientation) % 360;
        }
        mParameters.setRotation(rotation);
    }

從上述代碼可以看出,相機(jī)圖片的方向與設(shè)備的方向和相機(jī)的前后置有關(guān)。

對(duì)于設(shè)備的方向:
1.在不鎖屏的狀態(tài)下:我們可以通過 WindowManager.getDefaultDisplay().getRotation() 獲得。但是需要注意的是這里的 orientation 與屏幕的 rotation 不同。根據(jù)文檔的描述可以看出設(shè)備在自然豎直狀態(tài)下順時(shí)針旋轉(zhuǎn)270度所得 orientation 為270度,而此時(shí)的屏幕 rotation 為90度??梢妰烧呤且粋€(gè)簡(jiǎn)單的互補(bǔ)關(guān)系。
2.在鎖屏狀態(tài)下:我們依然可以通過 SensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),SensorManager.SENSOR_DELAY_NORMAL) 來獲得。

對(duì)于相機(jī)的前后置:
1.當(dāng)相機(jī)為前置相機(jī)時(shí):需要額外對(duì)采集到的圖像做鏡像翻轉(zhuǎn),這是由于底層相機(jī)在傳遞前置攝像頭預(yù)覽數(shù)據(jù)時(shí)做了水平翻轉(zhuǎn)變換,因?yàn)檫@里需要再做一次水平翻轉(zhuǎn)。
**2.當(dāng)相機(jī)為后置相機(jī)時(shí): **則不需要做額外的處理。

依舊舉例來說明:
當(dāng)我們以圖1中的①的姿勢(shì)握持手機(jī)并使用前置相機(jī)拍攝時(shí):
orientation = (270 + 45) / 90 * 90 = 270
rotation = (270 + 90) % 360 = 0
此時(shí)我們不需要對(duì)相機(jī)傳感器采集到的圖像進(jìn)行任何旋轉(zhuǎn),直接交給對(duì)應(yīng)的 View 去顯示即可。
當(dāng)我們以圖1中的③的姿勢(shì)握持手機(jī)并使用后置相機(jī)拍攝時(shí):
orientation = (90 + 45) / 90 * 90 = 90
rotation = (90 + 90) % 360 = 180
此時(shí)我們需要對(duì)相機(jī)采集到的圖像順時(shí)針旋轉(zhuǎn)180度,即將圖1 中的③順時(shí)針旋轉(zhuǎn)180度再交給對(duì)應(yīng)的 View 去顯示,這樣就符合我們的直觀感受了。

Camera 方向總結(jié):
對(duì)于相機(jī)傳感器方向、相機(jī)預(yù)覽方向以及圖片預(yù)覽方向這三個(gè)方向來說。除相機(jī)傳感器方向是我們不能夠通過代碼控制的之外,后兩個(gè)方向都是都是我們可以去設(shè)置的。另外,相機(jī)的預(yù)覽方向和圖片的預(yù)覽方向二者無關(guān),它們都只和傳感器的方向有關(guān)。

方向說完了我們?cè)賮砜纯创笮。⊿ize)。

2. 大小

2.1 SurfaceView/TextView 的大小

即用來展示圖像的 View 的大小,一般以其一邊為基準(zhǔn)設(shè)置其高寬比(aspect ratio),可以通過重寫 onMeasure 方法實(shí)現(xiàn)。

2.2 PreViewSize

2.2.1 什么是 PreViewSize:

相機(jī)硬件提供的預(yù)覽幀數(shù)據(jù)尺寸。預(yù)覽幀數(shù)據(jù)傳遞給 view,實(shí)現(xiàn)預(yù)覽圖像的顯示。其寬高寬比要盡可能的與 View 的高寬比相同,以免造成顯示的比例失調(diào)。

2.2.2 如何設(shè)置 PreViewSize:

在 Camera 中我們可以通過Camera.Parameters#setPreviewSize 方法設(shè)置 PreviewSize。在 Camera2 中如果我們使用 TextureView ,設(shè)置預(yù)覽 PreViewSize 的過程相當(dāng)于把預(yù)覽尺寸投射到 View 上。 同樣可以通過 TextureView#setTransform(matrix)來實(shí)現(xiàn),具體實(shí)現(xiàn)方法可以參照文末給出的開源項(xiàng)目 material-camera。

2.2.3 如何確定 PreViewSize:

目前所見過的一共2種方法思路:
1.挑選 PreViewSize 中寬高比與所給 Size 相同,且 PreViewSize 的長(zhǎng)寬大于等于所給的 Size 長(zhǎng)寬的一項(xiàng)。
2.挑選 PreViewSize 中寬高比與所給 Size 比列差值最小的一項(xiàng)。

public CameraController.Size getOptimalPreviewSize(List<CameraController.Size> sizes) {
        if( MyDebug.LOG )
            Log.d(TAG, "getOptimalPreviewSize()");
        final double ASPECT_TOLERANCE = 0.05;
        if( sizes == null )
            return null;
        CameraController.Size optimalSize = null;
        double minDiff = Double.MAX_VALUE;
        Point display_size = new Point();
        Activity activity = (Activity)this.getContext();
        {
            Display display = activity.getWindowManager().getDefaultDisplay();
            display.getSize(display_size);
            if( MyDebug.LOG )
                Log.d(TAG, "display_size: " + display_size.x + " x " + display_size.y);
        }
        double targetRatio = calculateTargetRatioForPreview(display_size);
        int targetHeight = Math.min(display_size.y, display_size.x);
        if( targetHeight <= 0 ) {
            targetHeight = display_size.y;
        }
        // Try to find the size which matches the aspect ratio, and is closest match to display height
        for(CameraController.Size size : sizes) {
            if( MyDebug.LOG )
                Log.d(TAG, "    supported preview size: " + size.width + ", " + size.height);
            double ratio = (double)size.width / size.height;
            if( Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE )
                continue;
            if( Math.abs(size.height - targetHeight) < minDiff ) {
                optimalSize = size;
                minDiff = Math.abs(size.height - targetHeight);
            }
        }
        if( optimalSize == null ) {
            // can't find match for aspect ratio, so find closest one
            if( MyDebug.LOG )
                Log.d(TAG, "no preview size matches the aspect ratio");
            optimalSize = getClosestSize(sizes, targetRatio);
        }
        if( MyDebug.LOG ) {
            Log.d(TAG, "chose optimalSize: " + optimalSize.width + " x " + optimalSize.height);
            Log.d(TAG, "optimalSize ratio: " + ((double)optimalSize.width / optimalSize.height));
        }
        return optimalSize;
    }

    private CameraController.Size getClosestSize(List<CameraController.Size> sizes, double targetRatio) {
        if( MyDebug.LOG )
            Log.d(TAG, "getClosestSize()");
        CameraController.Size optimalSize = null;
        double minDiff = Double.MAX_VALUE;
        for(CameraController.Size size : sizes) {
            double ratio = (double)size.width / size.height;
            if( Math.abs(ratio - targetRatio) < minDiff ) {
                optimalSize = size;
                minDiff = Math.abs(ratio - targetRatio);
            }
        }
        return optimalSize;
    }

2.3 PictureSize

2.3.1 什么是 PictureSize:

相機(jī)硬件提供的拍攝幀數(shù)據(jù)尺寸。拍攝幀數(shù)據(jù)可以生成位圖文件,最終保存成.jpg或者.png等格式的圖片。

2.3.2 如何設(shè)置 PictureSize:

在 Camera 中可以先通過 Camera.Parameters#getSupportedPictureSizes() 獲得設(shè)備所支持的 PictureSize,再通過 Camera.parameters#setPictureSize 方法設(shè)置最終輸出的 PictureSize。
在 Camera2 中可以先通過 StreamConfigurationMap.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) 方法獲得設(shè)備所支持的 PictureSize,再通過 ImageReader#newInstance(int width, int height, int format, int maxImages) 方法設(shè)置最終輸出的 PictureSize 以及圖像格式。

2.3.3 如何確定 PictureSize:

PictureSize 的高寬比也應(yīng)盡量與對(duì)應(yīng)的 View 的高寬比相同,以免造成預(yù)覽時(shí)的圖像與輸出后的圖像在比例上的不一致,給人很明顯的視覺上的不一致。另外 PictureSize 也決定了最終輸出的圖片的像素大小(即所占物理空間),這就需要結(jié)合實(shí)際的需求考慮了。

總結(jié):SurfaceView/TextureView 與 PreViewSize 以及 PictureSize 這三者的寬高比應(yīng)盡量相同,以免造成視覺上的不一致。前兩者決定預(yù)覽時(shí)我們所看見的效果,而 PictureSize 只最終決定生成的位圖文件的大小。

參考的開源項(xiàng)目:
opencamera-code
material-camera
CameraFragment

參考的博客
http://ticktick.blog.51cto.com/823160/1592267
https://juejin.im/entry/56aa36fad342d300542e7510
https://www.zybuluo.com/kmfish/note/269589

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容